ProGuard 使用详解

8,456 阅读6分钟

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 开发者想隐藏内部实现,则打包时也混淆,但同样需要提供打包时使用的混淆规则,供使用者选择使用。