入口
为了决定哪些代码要被保留哪些代码要出丢弃和混淆,必须指定入口点。这些入口点通常是 main方法,activity,service等。
-
在压缩阶段,Proguard从这些入口点开始递归确定哪些类或类成员要被使用,其余的都会被丢弃。
-
在优化阶段,ProGuard 会进一步优化代码。在其他优化中,可以将不是入口点的类和方法设为 private,static 或 final ,删除未使用的参数,并且可以内联一些方法。
-
在混淆阶段,ProGuard 会重新命名不属于入口点的类和类成员。在整个过程中,保证入口点仍然可以通过其原始名称访问。
查看 Proguard 输出结果
为了避免引入 bug 我们有必要对 结果进行检查。
在Android中,开启了混淆构建会在 /build/outputs/mapping/ 目录下会输出以下文件:
-
dump.txt 描述APK文件中所有类的内部结构
-
mapping.txt 提供混淆前后类、方法、类成员等的对照表
-
seeds.txt 列出没有被混淆的类和成员
-
usage.txt 列出被移除的代码
我们可以根据 seeds.txt 文件检查未被混淆的类和成员中是否已包含所有期望保留的,再根据 usage.txt 文件查看是否有被误移除的代码。
过滤器
ProGuard 为许多配置提供了不同方面的过滤选项:文件名称,目录,类别,软件包,属性,优化等。
过滤器是可以包含通配符的,以逗号分隔的,名称列表。
只有与列表中的项目匹配的名称才会通过过滤器。
每种配置的通配符可能有所不同,但以下通配符是通用的:
-
? 匹配名称中的任何单个字符。
-
* 匹配不包含包分隔符或目录分隔符的名称的任何部分
-
** 匹配名称的任何部分,可能包含任意数量的包分隔符或目录分隔符。
此外,名称前可以加上否定感叹号 !
排除名称与进一步尝试匹配后续名称。
因此,如果名称与过滤器中的某个项目相匹配,则会立即接受或拒绝该项目,具体取决于项目是否具有否定符。
如果名称与项目不匹配,则会针对下一个项目进行测试,依此类推。
它如果与任何项目不匹配,则根据最后一项是否具有否定符而被接受或拒绝。
如,"!foobar,*.bar" 匹配除了foobar之外的所有以bar结尾的名称。
下面以过滤文件具体举例。
文件过滤器
像通用过滤器一样,文件过滤器是逗号分隔的文件名列表,可以包含通配符。只有具有匹配文件名的文件被读取(在输入的情况下),或者被写入(在输出的情况下)。支持以下通配符:
-
? 匹配文件名字中的任何单个字符
-
* 匹配不包含目录分隔符的文件名的任何部分。
-
** 匹配文件名的任何部分,可以包含任意数目的目录分隔符。
例如 "java/**.class ,javax/**.class" 可以匹配 java和javax目录下所有的 class 文件。
此外,文件名前面可能带有感叹号'!'将文件名排除在与后续文件名匹配上。
例如 "!**.gif,images/**" 匹配images目录下所有除了 gif 的文件
关于更详细的用法 可以查看官方文档 filtering
keep 选项
-keep [,modifier,...] class specification
指定类和类成员(字段,方法)作为入口点被保留。
例如,为了保留一个程序,你要指定Main方法和类。为了保留一个库,你应该指定所有被公开访问的元素。
- 保留 main 类和 main 方法
-keep public class com.example.MyMain {
public static void main(java.lang.String[]);
}
- 保留所有被公开访问的元素
-keep public class * {
public protected *;
}
Note:如果你只保留了类,没有保留类成员,那么你的类成员将不会被保留
例如 有一个实体类
public class Product implements Serializable {
public static final int A = 1;
public static final int B = 2;
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
规则配置如下
# 保留 Product类
-keep class cn.sintoon.camera.Product
usage.txt文件中有以下内容 ,可以看到 类中的成员全部被移除了
cn.sintoon.camera.Product:
public static final int A
public static final int B
private java.lang.String name
private java.lang.String url
16:16:public java.lang.String getName()
20:21:public void setName(java.lang.String)
24:24:public java.lang.String getUrl()
28:29:public void setUrl(java.lang.String)
-keepclassmembers [,modifier,...] class specification
指定要保留的类成员,前提是它们的类也被保留了。
例如,你想保留实现了 Serializable 接口的类中的所有 serializable 方法和字段。
-keepclassmembers class * implements java.io.Serializable {
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
Note: 注意字段类型带上包名; String 类型为 java.lang.String;另外,如果只保留了类成员没有保留类跟没有保留一样
还是拿上面那个例子,改一下规则
-keepclassmembers class * implements java.io.Serializable{
private String name;
public String getName();
public static final int A;
}
再看 usage.txt 类都被移除了,保留字段没毛线用。
cn.sintoon.camera.Product
-keepclasseswithmembers [,modifier,...] class specification
指定要保留的类和类成员,条件是所有指定的类成员都在。
例如,你要保留程序中所有的主程序,不用显示的列出。
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
还是用上面那个例子,保留住类和所有的类成员
-keepclasseswithmembers class cn.sintoon.camera.Product{
public static final int A;
public static final int B;
private java.lang.String name;
private java.lang.String url;
public java.lang.String getName();
public void setName(java.lang.String);
public java.lang.String getUrl();
public void setUrl(java.lang.String);
}
看 seeds.text 中就会出现这个类和类成员
cn.sintoon.camera.Product
cn.sintoon.camera.Product: int A
cn.sintoon.camera.Product: int B
cn.sintoon.camera.Product: java.lang.String name
cn.sintoon.camera.Product: java.lang.String url
cn.sintoon.camera.Product: java.lang.String getName()
cn.sintoon.camera.Product: void setName(java.lang.String)
cn.sintoon.camera.Product: java.lang.String getUrl()
cn.sintoon.camera.Product: void setUrl(java.lang.String)
Note:一定要注意指定的类成员必须存在,如果不存在的话,这个规则相当于没有配,一点作用没有
-keepnames class specification
-keep,allowshrinking class specification的简写
指定要保留名称的类成员和类成员(如果它们在压缩阶段未被删除)。
例如,你可能希望保留实现 Serializable 接口的类的所有类名,以便处理后的代码与任何原始序列化的类保持兼容。
完全不用的类仍然可以删除。只有在混淆时才适用。
-keepnames class * implements java.io.Serializable
Note: 前提是在压缩阶段没有被删除掉,这里相当于使用了修饰符 allowshrinking
-keepclassmembernames class specification
-keepclassmembers,allowshrinking class specification 的简写
指定要保留名称的类成员(如果它们在压缩阶段未被删除)。
例如,在处理由JDK 1.2或更早版本编译的库时,可能希望保留合成类$方法的名称。
所以当处理使用处理过的库的应用程序时,混淆器可以再次检测到它(尽管ProGuard本身不需要这个)。
只有在混淆时才适用。
-keepclassmembernames class * {
java.lang.Class class$(java.lang.String);
java.lang.Class class$(java.lang.String, boolean);
}
Note: 前提是在压缩阶段没有被删除掉,这里相当于使用了修饰符 allowshrinking
-keepclasseswithmembernames class specification
-keepclasseswithmembers,allowshrinking class specification 的简写
指定要保留名称的类和类成员,条件是所有指定的类成员都存在于收缩阶段之后。
例如,可能希望保留所有本机方法名称和类别的名称,以便处理的代码仍可以与本机库代码链接。完全没有使用的本地方法仍然可以被删除。
如果使用了一个类文件,但它的本地方法都不是,它的名字仍然会被混淆。只有在混淆时才适用。
-keepclasseswithmembernames,includedescriptorclasses class * {
native ;
}
Note: 前提是在压缩阶段没有被删除掉,这里相当于使用了修饰符 allowshrinking
-printseeds [filename]
指定详尽列出由各种-keep选项匹配的类和类成员。列表打印到标准输出或给定文件。该列表可用于验证是否真的找到了预期的类成员,尤其是在使用通配符的情况下。
例如,您可能想要列出您保存的所有应用程序或所有小程序。
参考上面说的 seeds.txt
-whyareyoukeeping class specification
指定打印详细信息,说明为什么给定的类和类成员正在压缩步骤中。
如果想知道为什么某些给定元素出现在输出中,这会很有用。
一般来说,可能有很多不同的原因。
此选项为每个指定的类和类成员打印最短的方法链到指定的种子或入口点。
在当前的实施中,打印出的最短链有时可能包含循环扣除 - 这些并不反映实际收缩过程。
如果指定了 -verbose 选项,则跟踪包括完整的字段和方法签名。只适用于压缩。
压缩规则
-dontshrink
指定不被压缩的类文件。
默认情况下压缩是开启的,除了用各种用 keep
选项直接或间接用到的类或类成员,其他的都会被移除。
压缩步骤通常在优化之后,因为某些优化可能会打开已经删除的类或类成员。
-printusage [filename]
指定列出移除的死代码。该列表打印到标准输出或给定文件。
参考上面说的 usage.txt
例如,您可以列出应用程序的未使用代码。只适用于压缩。
优化规则
-dontoptimize
指定不优化输入类文件。默认情况下,优化已启用;所有方法都在字节码级别进行了优化
-optimizationpasses n
指定要执行的优化传递的数量。
默认情况下,执行一次传递。多次通行可能会导致进一步的改进。如果在优化后没有找到改进,则优化结束。只适用于优化。
混淆规则
-dontobfuscate
指定不混淆输入的类文件。
默认情况下,混淆是开启的,类和类成员会被改成新的短随机名称,除了各种-keep选项列出的名称外。
内部属性对于调试很有用,例如源文件名,变量名和行号被删除。
-printmapping [filename]
指定将旧名称映射到已重命名的类和类成员的新名称的映射。映射打印到标准输出或给定文件。
例如,它是后续增量混淆所必需的,或者如果想再次理解混淆的堆栈跟踪。只有在混淆时才适用。
参考 上面说的 mapping.txt。
-useuniqueclassmembernames
指定将相同的混淆名称分配给具有相同名称的类成员,并将不同混淆名称分配给名称不同的类成员(对于每个给定的类成员签名)。
没有这个选项,更多的类成员可以被映射到相同的短名称,比如'a','b'等等。
这个选项因此稍微增加了结果代码的大小,但是它确保了保存的混淆名称映射总是可以在随后的增量混淆步骤中受到尊重。
例如,考虑两个不同的接口,它们包含具有相同名称和签名的方法。如果没有此选项,这些方法可能会在第一个混淆步骤中获取不同的混淆名称。
如果添加了包含实现两个接口的类的补丁程序,则ProGuard必须在增量混淆步骤中为这两种方法强制执行相同的方法名称。
原始模糊代码已更改,以保持结果代码的一致性。在最初的混淆步骤中使用此选项,这种重命名将永远不是必需的。
该选项仅适用于混淆。
实际上,如果计划执行增量混淆,则可能希望完全避免压缩和优化,因为这些步骤可能会删除或修改部分代码,这些代码对于以后的添加至关重要。
-dontusemixedcaseclassnames
指定在混淆时不生成混合大小写的类名。
默认情况下,混淆的类名可以包含大写字符和小写字符的混合。
创建的这个完全可接受和可用的jars 只有在不区分大小写的文件系统(比如Windows)的平台上解压缩jar时,解压缩工具可能会让类似命名的类文件相互覆盖。
解压缩后自毁的代码!真正想在Windows上解压他们的jar的开发人员可以使用这个选项来关闭这种行为。
混淆的jars会因此变得稍大。
只有在混淆时才适用。
-keeppackagenames [package_filter]
指定不混淆给定的软件包名称。
可选过滤器是包名称的逗号分隔列表。包名可以包含?,*和**通配符,并且它们可以在!否定器。只有在混淆时才适用。
-flattenpackagehierarchy [package_name]
指定将所有重命名的软件包重新打包,方法是将它们移动到单个给定的父软件包中。如果没有参数或空字符串(''),程序包将移动到根程序包中。
该选项是进一步混淆软件包名称的一个示例。它可以使处理后的代码更小,更难理解。
只有在混淆时才适用。
-repackageclasses [package_name]
指定将所有重命名的类文件重新打包,方法是将它们移动到单个给定的包中。没有参数或者使用空字符串(''),该软件包将被完全删除。
该选项将覆盖 -flattenpackagehierarchy 选项。
这是进一步模糊软件包名称的另一个例子。
它可以使处理后的代码更小,更难理解。
其不推荐使用的名称是-defaultpackage。
只有在混淆时才适用。
警告:如果在别处移动它们,则在其包目录中查找资源文件的类将不再正常工作。如有疑问,请不要使用此选项,以免触及包装。
-keepattributes [attribute_filter]
指定要保留的任何可选属性。这些属性可以用一个或多个-keepattributes指令来指定。
可选过滤器是Java虚拟机和ProGuard支持的属性名称的逗号分隔列表。
属性名称可以包含?,*和**通配符,并且可以在之前加上!否定器。
例如,在处理库时,您至少应保留Exceptions,InnerClasses和Signature属性。
您还应该保留SourceFile和LineNumberTable属性以生成有用的混淆堆栈跟踪。
最后,如果你的代码依赖于它们,你可能需要保留注释。
只有在混淆时才适用。
# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
-keepparameternames
指定保留所保存方法的参数名称和类型。
该选项实际上保留了调试属性LocalVariableTable和LocalVariableTypeTable的修剪版本。
处理库时它可能很有用。
一些IDE可以使用这些信息来帮助使用该库的开发人员,
例如工具提示或自动完成。
只有在混淆时才适用。
-renamesourcefileattribute [string]
指定要放入类文件的SourceFile属性(和SourceDir属性)中的常量字符串。请注意,该属性必须首先出现,所以它也必须使用-keepattributes指令明确保留。
例如,您可能希望让处理过的库和应用程序生成有用的混淆堆栈跟踪。
只有在混淆时才适用
预校验 规则
-dontpreverify
指定不预先验证已处理的类文件。
默认情况下,如果类文件针对Java Micro Edition或Java 6或更高版本,则会对其进行预验证。
对于Java Micro Edition,需要进行预验证,因此如果指定此选项,则需要在处理的代码上运行外部预验证程序。
对于Java 6,预验证是可选的,但从Java 7开始,它是必需的。
只有在最终对Android时,它才不是必需的,因此您可以将其关闭以缩短处理时间。
-android
指定已处理的类文件针对Android平台。然后ProGuard确保一些功能与Android兼容。
例如,如果您正在处理Android应用程序,则应该指定此选项。
一般规则
-verbose
指定在处理期间写出更多信息。如果程序以异常终止,则此选项将打印出整个堆栈跟踪,而不仅仅是异常消息。
-dontnote [class_filter]
指定不打印有关配置中可能的错误或遗漏的注释,
例如类名中的拼写错误或缺少可能有用的选项。
可选的过滤器是一个正则表达式;
ProGuard不打印有关匹配名称的类的注释。
-dontwarn [class_filter]
指定不警告有关未解决的引用和其他重要问题。
可选的过滤器是一个正则表达式; ProGuard不打印关于具有匹配名称的类的警告。忽略警告可能是危险的。
例如,如果处理确实需要未解决的类或类成员,则处理后的代码将无法正常工作。
只有在你知道自己在做什么的情况下才使用此选项!
-ignorewarnings
指定打印任何关于未解决的引用和其他重要问题的警告,但在任何情况下都继续处理,忽略警告。
忽略警告可能是危险的。
例如,如果处理确实需要未解决的类或类成员,则处理后的代码将无法正常工作。
只有在知道自己在做什么的情况下才使用此选项!
-printconfiguration [filename]
指定使用包含的文件和替换的变量写出已解析的整个配置。结构打印到标准输出或给定文件。
这对于调试配置或将XML配置转换为更易读的格式有时会很有用。
-dump [filename]
指定在任何处理后写出类文件的内部结构。结构打印到标准输出或给定文件。
例如,可能希望写出给定jar文件的内容,而不进行处理。
参考上面说的 dump.txt。
-addconfigurationdebugging
指定用调试语句来处理已处理的代码,这些语句显示缺少ProGuard配置的建议。
如果处理后的代码崩溃,那么在运行时获得实用提示可能非常有用,因为它仍然缺少一些反射配置。
例如,代码可能是使用GSON库序列化类,可能需要一些配置。通常可以将控制台的建议复制/粘贴到配置文件中。
警告:不要在发行版本中使用此选项,因为它将混淆信息添加到已处理的代码中。
keep 选项修饰符
includedescriptorclasses
指定-keep选项所保存的方法和字段的类型描述符中的任何类也应保存。
在保留方法名称时,这通常很有用,以确保方法的参数类型不会重命名。他们的签名保持完全不变,并与本地库兼容。
includecode
指定保持-keep选项所保存的字段的方法的代码属性也应该保留,即可能未被优化或模糊处理。这对于已优化或混淆的类通常很有用,以确保在优化期间未修改其代码。
allowshrinking
指定-keep选项中指定的入口点可能会压缩,即使必须另外保留它们。
也就是说,可以在压缩步骤中删除入口点,但如果它们是必需的,则它们可能未被优化或混淆。
allowoptimization
指定-keep选项中指定的入口点可能会被优化,即使它们必须另外保存。
也就是说,入口点可能会在优化步骤中被更改,但它们可能不会被删除或混淆。
此修饰符仅用于实现不寻常的要求。
allowobfuscation
指定在-keep选项中指定的入口点可能会被混淆,即使它们必须另外保存。
也就是说,入口点可能在混淆步骤中被重命名,但它们可能不会被删除或优化。
此修饰符仅用于实现不寻常的要求。
keep 选项之间的关系
压缩和混淆的各种-keep选项起初看起来有点混乱,但实际上它们背后有一个模式。
下表总结了它们之间的关系:
内容 | 被删除或重命名 | 被重命名 |
---|---|---|
类和类成员 | -keep | -keepnames |
只有类成员 | -keepclassmembers | -keepclassmembernames |
类和类成员,引用成员存在 | -keepclasseswithmembers | -keepclasseswithmembernames |
如果指定了一个没有类成员的类,ProGuard只保留该类及其无参数的构造函数作为入口点。它可能仍会删除,优化或混淆其他班级成员。
如果指定了一个方法,则ProGuard仅将该方法作为入口点进行保存。其代码可能仍会进行优化和调整。
类规范
类规范是类和类成员(字段和方法)的模板。它用于各种-keep选项和-assumenosideeffects选项中。相应的选项仅适用于与模板匹配的类和类成员。
模板的设计看起来非常类似于Java,并为通配符进行了一些扩展。为了理解语法,你应该看看这些例子,但这是对一个完整的正式定义的尝试:
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] |
(fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] |
(argumenttype,...) | classname(argumenttype,...) |(returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
方括号 “[]” 表示其内容是可选的。
省略号点“...”表示可以指定任意数量的前述项目。
垂直条“|”划定了两种选择。
非粗体括号“()”只是将属于规范的部分组合在一起。
缩进尝试澄清预期的含义,但在实际配置文件中,空白是不相关的。
class关键字指的是任何接口或类。interface 关键字限制匹配接口类。 enum关键字限制匹配枚举类。在 interface 或 enum 关键字前加上!将匹配限制为不是接口或枚举的类。
每一个类名字都必须是完全限定名,例如 java.lang.String 内部类用美元符号“$”分隔,例如java.lang.Thread$State。类名可以被指定为包含以下通配符的正则表达式:
-
? 匹配类名称中的任何单个字符,但不匹配包分隔符。例如 "com.example.Test?" 可以匹配 "com.example.Test1" 和 "com.example.Test2" 但不能匹配 "com.example.Test12"
-
* 匹配不包含包分隔符的类名的任何部分。例如 "com.example.*Test*" 能够匹配 "com.example.MyTest" 和 "com.example.MyTestProduct" 但不能匹配 "com.example.mxc.MyTest" 或者 "com.example.*" 能够匹配 "com.example" 但不能匹配 "com.example.mxc"
-
** 匹配类名称的任何部分,可能包含任意数量的包分隔符。例如,"**.Testz" 匹配除根包以外的所有包中的所有Test类。或者,"com.example.**" 匹配 "com.example" 中的所有类及其子包。
-
在相同的选项中匹配第n个匹配的通配符。例如,"com.example.*Foo<1>" 匹配"com.example.BarFooBar"。
为了获得更多的灵活性,类名实际上可以是逗号分隔的类名列表,可以加!。这个符号看起来不是很像java,所以应该适度使用。
为了方便和向后兼容,类名*指任何类,而不考虑它的包。
-
extends 和 **implements ** 通常用来限制使用通配符的类。目前他们是一样的。他们的意思是 只有继承或实现了给定类的类才有资格。给定的类本身不包含在这个集合中。如果需要,应该在单独的选项中指定。
-
@ 可用于将类和类成员限制为使用指定的注释类型进行注释的类。annotationtype 就像类名一样被指定。
-
除了方法参数列表不包含参数名称外,字段和方法在Java中的定义非常类似(就像在javadoc和javap等其他工具中一样)。这些规范还可以包含以下通配符通配符:
通配符 | 意义 |
---|---|
匹配任何构造方法 | |
匹配任何字段 | |
匹配任何方法 | |
* | 匹配任何方法和字段 |
请注意,上述通配符没有返回类型。只有
字段和方法也可以使用正则表达式来指定。名称可以包含以下通配符:
通配符 | 意义 |
---|---|
? | 匹配方法名的任何单个字符 |
* | 匹配方法名的任何部分 |
在相同的选项中匹配第n个匹配的通配符 |
类型可以包含以下通配符
通配符 | 意义 |
---|---|
% | 匹配任何原始类型(boolean,int 等,不包含 void) |
? | 匹配类名中的单个字符 |
* | 匹配类名中的任何部分但不包含包分隔符 |
** | 匹配类名中的任何部分但不包含包分隔符 |
*** | 匹配任何类型(原始类型或者非原始类型,数组或者非数组) |
--- | 匹配任何类型的任意数量的参数 |
在相同的选项中匹配第n个匹配的通配符。 |
请注意,?,*和**通配符永远不会匹配基本类型。
而且,只有***通配符才能匹配任何维度的数组类型。
例如,“** get *()”匹配“java.lang.Object getObject()”,但不匹配“float getFloat()”和“java.lang.Object [] getObjects()”。
-
也可以使用短类名(无包)或使用完整的类名来指定构造函数。和Java语言一样,构造函数规范有一个参数列表,但没有返回类型。
-
类访问修饰符和类成员访问修饰符通常用于限制通配类和类成员。它们指定必须为成员设置相应的访问标志以匹配。前面加 "!" 决定相应的访问标志应该被取消设置。
允许组合多个标志(例如,public static)。这意味着必须设置两个访问标志(例如 public static ),除非它们有冲突,在这种情况下,至少必须设置其中一个(例如至少public或 protected)。
ProGuard支持可能由编译器设置的其他修饰符 synthetic,bridge和varargs。
参考资料
-
https://www.guardsquare.com/en/proguard/manual/introduction
-
https://www.diycode.cc/topics/380