[译] 实用 ProGuard 规则示例

我在以前的文章中解释了 为何每一个人都应该将 ProGuard 用于他们的 Android 应用、怎么启用它以及在使用中可能面临的错误种类。这其中涉及不少理论,由于我认为理解基本原理以准备好处理任何潜在问题很是重要。html

我还在一篇单独的文章中谈到了 为 Instant App 构建配置 ProGuard 的很是具体的问题。前端

在这里,我想谈 ProGuard 规则在中型样例应用上的实用示例:出自 Nick ButcherPlaid.java

从 Plaid 中吸收的教训

Plaid 其实是研究 ProGuard 问题的一个很好的主题,由于它包含使用注解处理与代码生成、反射、Java资源加载和原生代码(JNI)的第三方库的混合体。我提取并记录下了一些适用于其余应用的实用建议:node

数据类

public class User {
  String name;
  int age;
  ...
}
复制代码

每一个应用可能都有某种数据类(也被称为 DMOs,模型等,取决于上下文以及它们处在应用架构中的位置)。关于数据对象的事实是,一般在某些时候他们将被加载或保存(序列化)到某些其余介质中,例如网络(HTTP 请求)、数据库(经过 ORM)、磁盘上的 JSON 文件或 Firebase 数据存储。android

许多简化序列化与反序列化这些字段的工具依赖于反射。GSON、Retrofit、Firebase —— 他们都检查数据类的字段名并把它们转换成另外一种表现形式(例如:{“name”: “Sue”, “age”: 28}),用于传输或存储。它们将数据读入 Java 对象时也是同理 —— 它们看到键值对 “name”:”John” 并尝试经过查找 String name 字段将其应用到 Java 对象上。ios

结论:咱们不能让 ProGuard 重命名或删除这些数据类的任何字段,由于它们必须与序列化的格式匹配。最好给整个类添加一个 @Keep 注解或者给全部模型添加通配符规则:git

-keep class io.plaidapp.data.api.dribbble.model.** { *; }
复制代码

警告:在测试你的应用是否容易受到这个问题的影响是可能会出错。例如,若是你在版本 N 的应用程序中将一个对象序列化成 JSON 并将其保存到磁盘而没有使用适当的 keep 规则,那么保存的数据可能看起来像这样:{“a”: “Sue”, “b”: 28}。由于 ProGuard 将你的字段重命名为 ab,因此一切看起来彷佛都有效,数据也会被正确地保存和加载。github

然而,当你再一次构建你的应用并发布版本 N+1 的应用时,ProGuard 可能会决定将你的字段重命名为某些其余的,好比 cd。所以,以前保存的数据将没法加载。数据库

首先你必须确保你有适当的 keep 规则。后端

从原生层调用的 Java 代码(JNI)

Android 的 默认 ProGuard 文件(你应该老是包括它们,它们有一些很是有用的规则)已经包含了针对在原生层实现的方法的规则(-keepclasseswithmembernames class * { native <methods>; })。遗憾的是,没有一种全能的方法能够保留从反方向调用的代码:从 JNI 到 Java。

利用 JNI,彻底有可能从 C / C++ 代码中构造 JVM 对象或者找到并调用 JVM 句柄的方法,并且事实上,Plaid 的一个库就是这样

结论:由于 ProGuard 只能审查 Java 类,因此它不会知道任何在原生代码中发生的使用。咱们必须经过 @Keep 注解或 -keep 规则来显式地保留这些类和成员的使用。

-keep, includedescriptorclasses
            class in.uncod.android.bypass.Document { *; }
-keep, includedescriptorclasses
            class in.uncod.android.bypass.Element { *; }
复制代码

从 JAR/APK 打开资源

Android 有其本身的资源系统,一般不会有 ProGuard 的问题。然而,在普通的 Java 中有另外一种 直接从 JAR 文件加载资源的机制。而且某些第三方库即便被编译到 Android 应用中也可能会使用这种机制(在这种状况下,它们将尝试从 APK 加载)。

问题是一般这些类会在本身的包名下寻找资源(这将转换为 JAR 或 APK 中的文件路径)。ProGuard 可能在混淆时重命名包名,所以在编译以后可能会发生类及其资源文件再也不位于最终 APK 中的同一包内。

要以这种方式识别加载资源,你能够在你的代码和任何你依赖的第三方库中查找 Class.getResourceAsStream / getResourceClassLoader.getResourceAsStream / getResource 的调用。

结论:咱们应该保留任何使用这种机制从 APK 加载资源的类的名字。

在 Plaid 中,实际上有两个 —— 一个在 OKHttp 库中,另外一个在 Jsoup 库中:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities
复制代码

如何为第三方库制定规则

在理想的世界里,每一个你使用的依赖都会在 AAR 中提供他们所须要的 ProGuard 规则。有时他们会忘记这样作或只发布 JAR,这些 JAR 没有标准的方式来提供 ProGuard 规则。

在这种状况下,在开始调试应用和制定规则以前,记得查看文档。一些库的做者提供推荐的 ProGuard 规则(例如在 Plaid 中使用的 Retrofit),这能够为你节省大量时间,并让你免受挫折。遗憾的是,不少库都不会这样(例如这篇文章中提到的 Jsoup 和 Bypass 的状况)。另请注意,在某些状况下,随库提供的配置只能在禁用优化的条件下起做用,所以若是你开启了优化,那么你可能踏入了未知领域。

那么当库没有提供规则时,如何制定规则呢? 我只能给你一些提示:

  1. 阅读构建输出和 logcat!
  2. 构建警告会告诉你添加哪些 -dontwarn 规则
  3. ClassNotFoundExceptionMethodNotFoundExceptionFieldNotFoundException 会告诉你添加哪些 -keep 规则

当你使用了 ProGuard 的应用崩溃时,你应该庆幸 —— 你将有一个开始调查的地方 :)

最糟糕的一类调试问题是你的应用工做了,可是例如屏幕没有显示或没有从网络加载数据。

在这里你须要去考虑我在本文中描述的一些场景并动手实践,甚至扎入第三方库的代码中并理解它可能失败的缘由,例如当它使用反射、拦截或 JNI 时。

调试与堆栈跟踪

ProGuard 默认会删除程序执行不须要的许多代码属性和隐藏元数据。其中一些对开发者实际上颇有用 —— 例如,你可能但愿保留堆栈跟踪的源文件名和行号,以使调试更容易:

-keepattributes SourceFile, LineNumberTable
复制代码

你也应当记得 保存构建发行版本时生成的 ProGuard 映射文件并将其上传到 Play 以便从用户遇到的任何崩溃中获得反混淆的堆栈跟踪。

若是要在使用 ProGuard 构建的应用中附加调试器来逐步执行方法代码,那么你还应该保留如下属性,以保留关于局部变量的一些调试信息(在 debug 构建类型中只须要这一行):

-keepattributes LocalVariableTable, LocalVariableTypeTable
复制代码

缩小的调试构建类型

构建类型的默认配置为 debug 不使用 ProGuard。这颇有道理,由于咱们但愿在开发时快速迭代和编译,但仍然但愿使用 ProGuard 来构建发布版本以使其尽量小和优化。

可是为了全面测试和调试任何 ProGuard 问题,最好像这样设置一个单独的、缩小的调试构建:

buildTypes {
  debugMini {
    initWith debug
    minifyEnabled true
    shrinkResources true
    proguardFiles getDefaultProguardFile('proguard-android.txt'),
                  'proguard-rules.pro'
    matchingFallbacks = ['debug']
  }
}
复制代码

使用这种构建类型,你将可以 链接调试器, 运行 UI 测试 (也在持续集成服务器上) 或 monkey 测试 你的应用,以便在尽量接近发布版本的构建上发现可能的问题。

结论:当你使用 ProGuard 时,你应当老是经过端到端测试,或者手动浏览应用的全部页面来看是否有任何缺失或崩溃,以对你的构建版本进行完全的 QA。

运行时注解,类型拦截

ProGuard 默认会删除代码中的全部注解甚至一些剩余的类型信息。对于一些库来讲,这不是个问题 —— 那些在编译时处理注解与生成代码的库(例如 Dagger2Glide 等等)可能之后程序运行时不须要这些注解。

还有另一类实际上在运行时检查注解或查看参数与异常的类型信息的工具。例如 Retrofit 就这样作,经过使用 Proxy 对象来拦截方法调用,而后查看注解和类型信息来决定什么内容该放入 HTTP 请求或从 HTTP 请求中读取。

结论:有时须要并保留在运行时而不是编译时被取的类型信息与注解。你能够查看 ProGuard 手册中的属性列表

-keepattributes *Annotation*, Signature, Exception
复制代码

若是你使用默认的Android ProGuard 配置文件(getDefaultProguardFile('proguard-android.txt')),那么前两个选项 —— 注解和签名 —— 是专门为你准备的。若是你没有使用默认的配置文件,那么你必须保证你本身添加它们(若是你知道你的应用须要他们,那么重复它们也没有什么坏处)。

将全部内容移至默认包

默认状况下,ProGuard 配置中不会添加 -repackageclasses 选项。若是你已经在混淆你的代码而且使用适当的 keep 规则解决了任何问题,那么你能够添加这个选项以进一步减少 DEX 的大小。它的工做原理是将全部类移至默认(根)包,从而实质上释放了被像 「com.example.myapp.somepackage」这样的字符串所占用的空间。

-repackageclasses
复制代码

ProGuard 优化

正如我以前提到的,ProGuard 能够为你作三件事:

  1. 它摆脱了未使用的代码,
  2. 重命名标识符从而使代码更小,
  3. 对整个程序进行优化。

在我看来,每一个人都应该尝试并配置他们的构建来使1. 和 2. 工做。

为了解锁 3.(额外的优化),你必须使用其余默认的 ProGuard 配置文件。在你的 build.gradle 中,将 proguard-android.txt 参数改成 proguard-android-optimize.txt

release {
  minifyEnabled true
  proguardFiles
      getDefaultProguardFile('proguard-android-optimize.txt'),
      'proguard-rules.pro'
}
复制代码

这会是你的发布构建更慢,但可能会让你的应用运行地更快和进一步缩小代码体积,这要归功于方法内联、类合并与更侵略性的代码删除等优化。但要作好准备,它可能会引入新的、更难诊断的错误,所以谨慎使用,若是有任何不起做用,务必禁用某些特定的优化或彻底禁用优化配置。

就 Plaid 来讲,ProGuard 优化干扰了 Retrofit 如何使用没有具体实现的代理对象,并剥离了一些实际须要的方法参数。我必须在个人配置中添加这一行:

-optimizations !method/removal/parameter
复制代码

你能够在 ProGuard 中找到 可能的优化列表以及如何禁用它们

什么时候使用 @Keep-keep

@Keep 的支持在默认的 Android ProGuard 规则文件中其实是经过一系列 -keep 规则实现的,所以它们基本上是等效的。指定 -keep 规则更灵活,由于它提供通配符,你也可使用不一样的变体,这些变体稍有不一样(-keepnames-keepclasseswithmembers 以及更多)。

每当须要一个简单的「保留这个类」或「保留这个方法」规则时,我实际上更喜欢在类或成员上添加 @Keep 注解的简单性,由于它离代码很近,几乎就像文档同样。

若是其余开发者想要在我以后重构代码,他们会当即知道被 @Keep 标记的类 / 成员须要特殊处理,而没必要记住和参考 ProGuard 配置而且冒着破坏某些东西的风险。IDE 中大部分的代码重构也应当自动保留类的 @Keep 注解。

Plaid 统计信息

这有一些来自 Plaid 的统计信息,它们展现了我经过使用 ProGuard 删除了多少代码。在有更多依赖和更大 DEX 的更复杂的应用上,节省的可能更多。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索