4、R8 vs ProGuard:差异与迁移指南

我记得刚入行那会儿,团队里所有人都在用 ProGuard。大家觉得它稳定、成熟,踩过的坑都能在 Stack Overflow 上找到答案。直到两年前我接手一个日活千万的 App,上线前发现构建耗时已经超过 25 分钟…… 嗯,那时候我才真正开始认真对比 R8 和 ProGuard。

说白了,R8 就是 Google 对 ProGuard 的全面升级——它把代码压缩、混淆、优化、脱糖和 DEX 编译全部打包成一个任务。你想想看,以前跑 ProGuard 要单独配一个 pass,现在 R8 一步到位,构建时间直接砍半。

4.1 核心差异:不只是快

先放一张我平时培训新人用的对比图。看完你就知道为什么我强烈建议迁移。

R8 vs ProGuard 差异与迁移路线图 ProGuard 传统流程 Java 源文件 → .class ProGuard 压缩/混淆 D8 脱糖 & DEX APK 打包 R8 一体化流程 Java/Kotlin 源文件 R8 压缩+混淆+优化 内置脱糖、内联、常量折叠 直接输出 DEX APK 打包 速度提升:R8 平均快 2~3 倍 APK 体积:额外减少 5%~10% 迁移步骤(5 步搞定) 1. 升级 AGP 至 4.0+,确保 Gradle 版本兼容 2. 将 build.gradle 中的 ProGuard 配置改为 R8 模式 3. 删除重复的 -keep 规则(R8 智能保留规则) 4. 处理反射调用:添加 -keep class 例外 5. 运行 lintRelease 检查,验证线上崩溃率

4.2 速度:R8 吊打 ProGuard

实测数据最说明问题。我拿同一个中型项目(约 500 个 module、80 万方法)做过对比:

对比维度 ProGuard R8 提升幅度
全量构建(release) 42 秒 18 秒 快 57%
增量构建 14 秒 3 秒 快 78%
APK 体积(压缩后) 28.5 MB 24.3 MB 减少 14.7%
运行时启动耗时 1.2 s 0.9 s 快 25%

为什么 R8 能跑这么快?因为 ProGuard 是独立进程,每次都需要把 .class 文件写入磁盘再读出;而 R8 直接跟 Gradle 的 Transform 流程整合,全程内存操作。我习惯在 CI 机器上跑 full release build,切到 R8 后构建时间从 4 分钟降到 1 分半——那个感觉,就像把老爷车换成了电动车。

4.3 优化深度:R8 做了更多

ProGuard 只做两件事:压缩(去掉无用代码)和混淆(重命名类和方法)。R8 在这个基础上又加了三把火:

  • 内联:把短方法的调用直接展开,减少调用栈,提升 JIT 编译命中率。
  • 常量折叠:如果某个变量最终值能确定,就直接替换成字面量,省去计算开销。
  • 尾部递归优化:将递归转成循环,避免栈溢出。

举个例子,你代码里写:

private static int add(int a, int b) { return a + b; }
int result = add(3, 5);

ProGuard 不会动它。R8 会直接变成 int result = 8。你说谁更聪明?

我的经验:如果你项目里用了大量的 Builder 模式或者工厂方法,R8 的内联优化效果尤其明显。曾经一个项目启动时调用了 2000 多个短方法,R8 帮我们内联掉了 40%,Application.onCreate() 执行时间从 600ms 降到了 380ms。

4.4 迁移指南:五步走,不翻车

说实话,迁移没那么可怕。我在 4 个项目上做过,只有一次翻车——因为某个三方库的 keep 规则写得太宽泛,R8 把它的接口类全干掉了。下面是我总结的迁移步骤:

  1. 升级 AGP 到 4.0 以上(推荐 7.4+)。R8 是 AGP 内置的,版本太老根本不会启用。
  2. 修改 build.gradle:把 proguardFiles 和 proguard-android-optimize.txt 保留,但移除 runtimeProguardFiles(R8 会自动处理)。
  3. 启用 R8 的全量优化:在 gradle.properties 里加一行
    android.enableR8.fullMode=true
    这样 R8 才会做内联和常量折叠。默认只做压缩和混淆。
  4. 清理过时的 keep 规则:ProGuard 时代很多规则是为了保险而写的“全包”规则,比如 -keep class com.example.** { *; }。R8 有自己的启发式保留策略,这类通配规则尽量删掉,只保留真正需要反射的类。
  5. 灰度测试:先在 beta 版本跑 3~5 天,检查 Breadcrumb 崩溃。关注 ClassNotFoundException 和 MethodNotFoundException。

曾经的血泪教训:我迁移一个即时通信 App 时,漏了一条 Gson 的 TypeToken 规则。R8 把泛型信息全干掉了,导致 JSON 解析全崩。记住!所有用到 TypeToken<T> {} 的匿名内部类,必须用 -keepclassmembers class * { ... } 保留。

4.5 避坑清单(我踩过的)

迁移过程中最容易踩的坑,我列了个清单:

  • 反射调用:R8 默认会删除只被反射使用的类。解决方案:用 -keep class com.example.reflection.** { *; }。
  • JNI 方法:如果你的 native 方法用 Java_com_example_ 这种命名,R8 不会重命名它们。但如果你用 RegisterNatives 动态注册,就一定要 keep。
  • 资源 ID 混淆:R8 默认不混淆资源 R 类,但如果你手动调用了 Resources.getIdentifier(),那就得保留 R 类。
  • Lambda 表达式:R8 对 Lambda 处理更激进,可能干掉没用到的 Lambda。如果依赖 Kotlin 协程,建议加上:
    -keepattributes *Annotation*
    -keep class kotlin.coroutines.** { *; }

核心建议:迁移完成后,用 ./gradlew assembleRelease --scan 生成构建扫描,观察 R8 的耗时分布。如果某个模块优化时间特别长,检查它的 -keep 规则是否过多。

4.6 什么时候不推荐迁移?

虽然我强烈推荐,但有两个例外:

  • 项目还在用 AGP 3.x 且无法升级(比如某些古董耳熟能详的银行 App)。
  • 业务非常依赖某些 ProGuard 特有功能,比如 -whyareyoukeeping 或者 -dump 输出。R8 目前还不完全支持这些调试命令。

不过说实话,如果你们团队还在用 AGP 3.x,可能更大的问题是 Gralde 版本兼容性,而不是 ProGuard 本身。我个人建议硬着头皮升级,长远来看是值得的。

最后说一句:R8 不是银弹。它不会自动修复你在 -keep 规则里犯的错,也不会帮你发现隐藏的反射调用。工具再好,也得看用工具的人。去试试吧,构建速度快起来之后,你会发现心情都变好了——至少在等编译喝咖啡的时间里,你可以少喝一口了。