Android中的Proguard

什么是ProGuard

ProGuard是一款免费的Java class文件收缩器、优化器、混淆器和预校验器。
ProGuard 已集成到 Android 构建系统,它通过移除无用的代码以及使用语义隐晦的名称来重命名类、字段和方法,从而达到压缩、优化和混淆代码的目的。最终您将获得一个较小的 .apk 文件,此文件更难于进行反向工程。由于 ProGuard 会使应用更难于进行反向工程,因此当应用使用对安全性要求极高的功能时(例如,当您向应用授予许可时),您必须使用此工具。当您在调试模式下构建应用时,就无需处理混淆后的代码。是否运行 ProGuard 完全由您决定。
Proguard通过删除代码中在运行期间不是绝对必要的信息,来防止静态分析。相比于把代码按照它原来的样子呈现出来,这已经前进了一大步,但它仍然是一种被动的方法。
ProGuard的提供者同时提供了另外两种针对Android平台的更高级的工具:Dexguard和Dexguard Enterprise,能够抵御静态分析和动态分析。它们兼容ProGuard的配置,并为额外的功能提供了额外的选项。如果你需要更高的安全性来防止你的代码被逆向分析,可以尝试这两个工具,不过它们是收费的。

三者的对比如下:

工作流程

ProGuard的功能包含收缩(shrink)、优化、混淆、预校验4个步骤,每一步的具体工作如下:

  • 收缩(shrink):检测和删除未使用的类、字段、方法、属性。ProGuard从这些种子开始递归决定哪些类以及类成员是被使用的。所有其他的类和类成员都被丢弃。
  • 优化(optimization):分析和优化各个方法的字节码。ProGuard更进一步优化代码。除了其他的一些优化操作外,还把不是入口点的类和方法变成private、static或者final,把未使用过的参数删除,把某些方法变成内联的(inline)。
  • 混淆(obfuscation):把剩下(即非入口点)的类、字段、方法重命名为简短而无意义的名字。在整个处理流程中,保持(keeping)入口点可以确保它们仍然能够被用原来的名字访问。
  • 预校验(preverification):向class中添加预校验信息,这对Java Micro Edition来说是必须的,同时也能够提高代码在Jave 6上的启动速度。预校验步骤是唯一不需要知道哪些是入口点的一个步骤。

其中收缩、优化、混淆这3步使得代码体积更小、更高效、更难以被逆向工程。4个步骤中的每一步都是可选的。

入口点(Entry points)

从上面的描述可以知道,为了确定哪些代码需要保留,哪些代码可以丢弃或者混淆,就必须为你的代码指定一个或多个入口点(Entry point)。这些入口点通常是包含main方法的类、applet或者midlet等等。

反射(Reflection)

反射(reflection)和内省(introspection)对于任何对代码的自动处理都会表现出特别的问题。在ProGuard中,你的代码中被动态(也就是通过名字)创建或调用的类和类成员也都必须被指定为入口点。

比如,Class.forName()在运行时可能指向任何类。一般不可能预见到哪些类必须被以它们原来的名字保留,因为这些类的名字可能是从一个配置文件中读取到的。因此你必须在你的ProGuard配置中用简单的-keep选项来指定它们。

不过Proguard会自动为你检测并处理下面这些情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
● Class.forName("SomeClass")
● SomeClass.class
● SomeClass.class.getField("someField")
● SomeClass.class.getDeclaredField("someField")
● SomeClass.class.getMethod("someMethod", new Class[] {})
● SomeClass.class.getMethod("someMethod", new Class[] { A.class })
● SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
● SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
● SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
● SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
● AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
● AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
● AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")

被引用的类和类成员在收缩阶段(shrinking phase)会被保留,字符串参数在混淆阶段(obfuscation phase)会被合理地更新。
除此之外,如果keeping某些类或类成员看起来是必要的,ProGuard就会提供一些建议。比如,ProGuard会对(SomeClass)Class.forName(variable).newInstance()这样的情况进行提示,指出这里的SomeClass类或接口以及它的实现类需要被原样保留。然后你就可以据此对你的ProGuard文件进行修改。

在Android Studio中使用ProGuard

在Android Studio中,如果Build Type通过设置minifyEnabled属性来配置运行ProGuard,那么ProGuard插件(ProGuard plugin)就会自动被Android插件(Android plugin)使用,并自动创建task。配置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //sample 1
}
}

productFlavors {
flavor1 {
}
flavor2 {
proguardFile 'some-other-rules.txt' //sample 2
}
}
}

在Android SDK中(具体路径是${sdk.dir}/tools/proguard/)存在2个默认的规则文件(rules files):

  • proguard-android.txt
  • proguard-android-optimize.txt

使用getDefaultProguardFile()会返回文件的完整路径。这2个文件除了是否开启优化(optimization)上不同,其他都是完全相同的。这2个默认文件(实际使用中只选择其中一个)定义了 ProGuard 会如何优化和混淆代码,默认的配置文件只涵盖一般的使用情形,针对自己具体的项目ProGuard的配置是需要实际情况进行定制的。
Android Studio在新建项目时,会在module的根目录下自动生成proguard-rules.pro文件,如果你需要自定义Proguard的配置,不需要去修改sdk下的那2个默认配置文件,只需要修改你的module根目录下的proguard-rules.pro就可以了。

配置 ProGuard

在某些情况下,proguard.cfg 文件中的默认配置足以满足您的需求。不过,在很多情况下,ProGuard 很难做出正确分析,因此可能会移除它认为无用而实际上您的应用却需要的代码。部分示例如下:

  • 一个只在 AndroidManifest.xml 文件中引用的类
  • 一个通过 JNI 调用的方法
  • 动态引用的字段和方法

默认的 proguard.cfg 文件旨在涵盖一般的使用情形,但您可能会遇到异常情况,例如 ClassNotFoundException(此异常情况会在 ProGuard 删除您的应用调用的整个类时发生)。

您可以通过在 proguard.cfg 文件中添加一个-keep行,来修复因 ProGuard 在删除代码而造成的错误。例如:

1
-keep public class <MyClass>

在使用 -keep 选项时,您既有许多选择也有不少需要注意的方面,因此我们强烈建议您阅读 ProGuard 手册,详细了解如何自定义您的配置文件。该手册中的“Keep 选项概述”和“示例”部分尤其有用;问题排查部分则概述了在 ProGuard 删除代码后您可能会遇到的其他常见问题。

混淆结果

ProGuard 在运行后会输出以下文件:

①dump.txt
描述 .apk 文件中所有类文件的内部结构。一个dump文件的示例片段如下:

②mapping.txt
列出原始与混淆后的类、方法和字段名称之间的对应关系。如果您从发布版本收到问题报告,则必须使用此文件,因为通过它可将混淆后的堆栈跟踪信息转换为原始的类、方法和成员名称。有关详情,请参阅解码混淆后的堆栈跟踪信息
一个mapping.txt的片段如下:

从图中可以看出,类和类成员的名字都被替换成了无意义的字母。

③seeds.txt
列出未混淆的类和成员。一个示例文件片段如下:

④usage.txt
列出从 .apk 删除的代码。一个示例文件片段如下:

这些文件都位于以下目录中:

1
<project_root>/<module_root>/build/outputs/mapping/

注意:每当您在发布模式下构建版本时,这些文件都会被 ProGuard 最新生成的文件覆盖。请在每次发布应用时为这些文件保存一份副本,以便反混淆来自发布版本的问题报告。

每次向用户发布应用时,都请保存所发布版本的 mapping.txt 文件。这样一来,如果用户遇到问题,并向您提交混淆后的堆栈跟踪信息,您就可以利用为每个发布版本保存的 mapping.txt 文件副本调试问题。每当您构建发布版本时,项目的 mapping.txt 文件都会被覆盖,因此您必须谨慎保存所需的版本。

例如,假设您发布了某个应用,并继续开发该应用的新功能,以便将来发布新版本。之后不久您使用 ProGuard 构建发布版本。此版本覆盖了之前的 mapping.txt 文件。之后,某位用户提交了问题报告,其中包含来自当前已发布的应用的堆栈跟踪信息。但您已无法调试该用户的堆栈跟踪信息,因为与该用户设备上的版本相关联的 mapping.txt 文件已被覆盖。除此之外,其他一些情况也可能会导致您的 mapping.txt 文件被覆盖。因此,如果您预计需要进行调试,请务必在每次发布应用时都保存一份副本。

如何保存 mapping.txt 文件由您自行决定。例如,您可以将其重命名以使其名称中包含版本号,也可以对其(连同源代码一起)进行版本管理。

解码混淆后的堆栈跟踪信息

当混淆后的代码输出堆栈跟踪信息时,方法名称会被混淆,即便仍能进行调试,难度也会很大。幸运的是,ProGuard 在每次运行时都会输出一个 //build/outputs/mapping/mapping.txt 文件,其中会显示与混淆后的名称相对应的原始的类、方法和字段名称。

Windows 上的 retrace.bat 脚本以及 Linux 或 Mac OS X 上的 retrace.sh 脚本可以将混淆后的堆栈跟踪信息转换成可读文件,此文件位于 /tools/proguard/ 目录中。执行 retrace 工具的语法如下:

1
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

1
retrace.bat -verbose mapping.txt obfuscated_trace.txt

如果您不为“”指定值,retrace 工具会从标准输入中读取。

其他选项(options)

Proguard拥有很多可用的选项,可以通过Proguard规则文件进行配置。

最常用的是keep选项,如果需要显示指定某个类要保留,可以使用keep来实现,比如:

1
-keep class com.android.support.** {*;}

如果你的代码需要打印堆栈信息,而混淆后的代码产生的堆栈信息可能无法正确获取行号和类信息,如果你需要这些信息来进行分析,可以强制保留这些信息:

1
-keepattributes SourceFile,LineNumberTable

当你引用第三方库时,往往要同时使用keep和dontwarn,比如:

1
2
3
-libraryjars libs/log4j.jar
-dontwarn org.apache.log4j.*
-keep class org.apache.log4j.** { *;}

将-dontwarn和-keep 结合使用,意思是保持包里面的所有类和所有方法而不混淆,接着还叫ProGuard不要警告找不到这个包里面的类的相关引用。

ProGuard有数量众多的options可用,如果需要查询各个option的用法,可以参考ProGuard-Usage

文章目录
  1. 1. 什么是ProGuard
  2. 2. 工作流程
    1. 2.1. 入口点(Entry points)
    2. 2.2. 反射(Reflection)
  3. 3. 在Android Studio中使用ProGuard
    1. 3.1. 配置 ProGuard
    2. 3.2. 混淆结果
  4. 4. 解码混淆后的堆栈跟踪信息
  5. 5. 其他选项(options)