4、R8 vs ProGuard:差异与迁移指南
我记得刚入行那会儿,团队里所有人都在用 ProGuard。大家觉得它稳定、成熟,踩过的坑都能在 Stack Overflow 上找到答案。直到两年前我接手一个日活千万的 App,上线前发现构建耗时已经超过 25 分钟…… 嗯,那时候我才真正开始认真对比 R8 和 ProGuard。
说白了,R8 就是 Google 对 ProGuard 的全面升级——它把代码压缩、混淆、优化、脱糖和 DEX 编译全部打包成一个任务。你想想看,以前跑 ProGuard 要单独配一个 pass,现在 R8 一步到位,构建时间直接砍半。
4.1 核心差异:不只是快
先放一张我平时培训新人用的对比图。看完你就知道为什么我强烈建议迁移。
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 把它的接口类全干掉了。下面是我总结的迁移步骤:
- 升级 AGP 到 4.0 以上(推荐 7.4+)。R8 是 AGP 内置的,版本太老根本不会启用。
- 修改 build.gradle:把
proguardFiles和proguard-android-optimize.txt保留,但移除runtimeProguardFiles(R8 会自动处理)。 - 启用 R8 的全量优化:在 gradle.properties 里加一行
这样 R8 才会做内联和常量折叠。默认只做压缩和混淆。android.enableR8.fullMode=true - 清理过时的 keep 规则:ProGuard 时代很多规则是为了保险而写的“全包”规则,比如
-keep class com.example.** { *; }。R8 有自己的启发式保留策略,这类通配规则尽量删掉,只保留真正需要反射的类。 - 灰度测试:先在 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 规则里犯的错,也不会帮你发现隐藏的反射调用。工具再好,也得看用工具的人。去试试吧,构建速度快起来之后,你会发现心情都变好了——至少在等编译喝咖啡的时间里,你可以少喝一口了。