Proguard 是一个适用于 Java 平台混淆代码的工具,也可以用于 Android,虽然我们直接称为混淆,实际上 Proguard 包括 shrink(压缩),optimize(优化),obfuscate(混淆),preverify(预校验)四步。
- shrink: 检测并移除没有用到的类,变量,方法和属性;
- optimize: 优化代码,非入口节点类会加上 private/static/final, 没有用到的参数会被删除,一些方法可能会变成内联代码。
- obfuscate: 使用短又没有语义的名字重命名非入口类的类名,变量名,方法名。入口类的名字保持不变。
- preverify: 预校验代码是否符合 Java1.6 或者更高的规范(唯一一个与入口类不相关的步骤)
常用语法
-keep
保护类及类成员不被压缩和混淆
-keep public class com.example.MyMain {
public static void main(java.lang.String[]);
}
保护 MyMain 类及 main 方法
-keepclassmembers
在指定类被保护的情况下,保护类成员不被压缩和混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
保护所有 Activty 子类中参数为 View 类的方法
-keepclasswithmembers
存在所有指定类成员的情况下,保护类和类成员不被压缩和混淆
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
保护所有包含 main 方法的类及其 main 方法
-keepnames
保护类及类成员不被混淆(只在混淆阶段生效,允许被压缩移除,不能被重命名)
-keepnames class * implements java.io.Serializable
保护所有实现了 Serializable 接口的类不被重命名
-keepclassmembernames
如果指定的类成员没有被压缩,保护它不被重命名,使用较少
-keepclasseswithmembernames
在压缩完代码后,如果所有指定类成员存在,则保护指定类和类成员不被混淆
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
如果存在 native 方法,则保护类和该 native 方法不被重命名, 使用 includedescriptorclasses 可以保证方法参数和返回值也不被重命名
总结
| keep | From being removed or renamed | From being renamed |
|---|---|---|
| Classes and class members | -keep | -keepnames |
| Class members only | -keepclassmembers | -keepclassmembernames |
| Classes and class members, if class members present |
-keepclasseswithmembers | -keepclasseswithmembernames |
- 如果只指定了类名,没有类成员,那只会保护类和类的无参构造函数
-keep class android.support.annotation.Keep
- 如果指定了某个方法,只会保护这个方法,方法内的代码仍然会被优化
通配符
指定类时,可以使用如下通配符
-
class 关键字表示任意的类或接口
-
interface 关键字只表示接口
-
enum 关键字表示枚举类
-
interface 和 enum 关键字可以加上 !表示除...之外
-
?匹配任意字符,不包括包分隔符
-
* 匹配任意多个字符,不包括包分隔符
-
** 匹配任意多个字符,包括包分隔符
-
为了向后兼容,* 也可以表示任意的类,包括包分隔符
-
extends 和 implements 关键字是等效的,表示继承或实现 A 的类,但不包括 A 本身
-
@ 关键字用于表示使用指定注解修饰的类和类成员
指定类的成员时,可以指定如下通配符
- <init> 匹配任意的构造器
- <fileds> 匹配任意的成员变量
- <methods> 匹配任意的方法
- * 匹配任意的成员变量或方法
- 上述通配符不含返回类型,只有<init>有参数列表
除了使用上述全能通配符以外,同样可以使用常用表达式,此时可以使用 ? 和 * 通配符
指定修饰符的类型时,可以使用如下通配符
- % 匹配基本类型
- ? 匹配任一字符
- * 匹配任意多个字符,不含包分隔符
- ** 匹配任意多个字符,包含包分隔符
- *** 匹配任意类型(基本或非基本,数组或非数组)
- ... 匹配任意数量、任意类型的参数
? * 和 ** 不会匹配基本类型
可以使用权限控制符帮助匹配(例如 public static)
在Android平台的使用
只需要在gradle中配置
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
即可开启混淆。
混淆的配置文件由三部分组成
一个是 proguard-android,在新版本的构建工具中,它随 andorid 的 gradle 插件一起打包,在与 app 平级的 build/intermedates/proguard-files 目录可以找到,里面配置了一份所有 Android 应用通用的混淆规则;
一个是 aapt_rules, 由 aapt 在打包时生成,在 app/build/intermediates/proguard-rules//aapt_rules.txt 可以找到,这里配置了所有在 manifest 和 xml 使用的类的 keep 规则;
一个是proguard-rules.pro,即开发者自定义的配置规则.
最终的配置规则由这三部分组合而成,这也是为什么即使开发者不定义任何规则,也可以完成混淆。
- 由于 dex 的特殊性,Android 平台一般不需要 optimize(优化) 和 preverify (预校验) 这两步,
- 在经过充分测试的情况下,可以开启 optimize,
- 预校验这一步有疑问,在老版本的默认的 proguard-android 中,是关闭了预校验的,Proguard 官网的 Android 示例也关掉了,但是在 android gradle 插件 3.1.3 版本上测试时,默认的 proguard-android 没有去掉预校验这一行,尚不清楚原因。
使用时,可以将 proguard-rules.pro 文件中的配置全部复制到自定义配置中,理解默认配置每一条规则的作用,遇到问题时更容易定位。
Proguard 输出的文件
- dump.txt 说明 APK 中所有类文件的内部结构。
- mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换。
- seeds.txt 列出未进行混淆的类和成员。
- usage.txt 列出从 APK 移除的代码。
保留行号
-keepattributes SourceFile,LineNumberTable 保留行号信息
-renamesourcefileattribute SourceFile 隐藏类名文件信息
这样配置可以在错误堆栈中保留行号信息同时隐藏类名,方便定位问题
SDK 的混淆
sdk 的混淆是最容易出错的地方,很多 sdk 的开发者对 Proguard 不清楚,不向使用者提供混淆规则或是直接提供暴力 keep 所有内容的规则
实际上,sdk 的混淆规则分两种
- 一是 sdk 本身必须 keep 的部分,如使用了反射
- 二是公开的 api,必须 keep 才能给外部使用
对 sdk 的使用者,可以从中选择适合自己的规则(因为不是所有的 api 都会用到)
合理的做法是 sdk 开发者打包时不混淆,而是将混淆规则通过 consumeProguarFiles 提供给调用者使用,因为 consumeProguarFiles 提供的规则最终是会影响到整个工程的,里面的规则最好不使用通配符,而是严格限定只 keep 指定的内容
如果 sdk 开发者想隐藏内部实现,则打包时也混淆,但同样需要提供打包时使用的混淆规则,供使用者选择使用。