R8
# Shrink code / code optimize
本页讨论 Android 应用构建中的 R8。配置示例采用 AGP 8.x 的 Groovy DSL;不同 AGP 版本的默认行为与 DSL 可能变化,使用时对照项目版本。

构建流程与作用
Android 构建中的 R8 读取应用及依赖的 class 字节码、入口与 keep 规则,进行分析和优化并生成 DEX。不能把 ProGuard 的“input jar → preverify → output jar”示意流程直接当作 Android R8 的实际构建输出。
| 操作 | 作用 |
|---|---|
| Shrink | 根据引用关系、入口及规则移除不可达代码 |
| Optimize | 重写代码,例如内联、简化表达式、类合并;具体结果取决于完整程序和配置 |
| Obfuscate | 缩短类、字段和方法名称,生成映射以便还原堆栈 |
代码入口并非只有 MainActivity,还包括 Manifest 声明的组件和 keep 规则指定的代码。反射等动态访问不能仅依靠普通静态调用图发现,需要适当的规则。
# R8 优化详解
# Shrink(代码缩减)
AGP 8.x 中,release 的 minifyEnabled true 启用 R8 代码处理。资源缩减由 shrinkResources 等配置控制,不能把资源缩减与代码缩减视为同一个开关。
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
2
3
4
5
6
7
8
9
10
示例假设模块中存在 proguard-rules.pro,用于维护自定义规则。默认优化配置已经提供优化规则,不需要额外创建名为 proguard-optimize.pro 的文件作为“开启优化”的必要条件。
Keep 规则示例
# 例:保留确实被反射访问的类及成员
-keep class com.example.MyClass { *; }
# 当所属类被保留时,保留这个成员
-keepclassmembers class com.example.MyClass {
public void myMethod();
}
2
3
4
5
6
7
-keepclassmembers 不会单独阻止整个所属类被删除。规则必须结合真实访问方式验证;不应为省事而保留整个依赖包,过宽的规则会限制缩减和优化。
# Optimize(代码优化)
AGP 8.x 示例中的 proguard-android-optimize.txt 用于启用优化配置;含有 -dontoptimize 的配置会禁止代码优化,需要检查最终合并的规则。不能笼统地说 optimize 必须通过额外配置文件单独开启。
AGP 8.0 起,R8 full mode 默认启用;它与 minifyEnabled、具体优化规则是不同维度的设置。升级 AGP 或调整规则后应重新测试,而不是沿用旧版本观察结果。
方法是否内联、字段或类是否移除,取决于可达性、调用关系、keep 规则及优化器版本。public 修饰符本身不意味着代码必然保留;包级私有也不意味着代码必然删除。应用对外暴露的反射入口、JNI、序列化等契约需要有针对性的规则。
# 性能提升
不使用未经测量的固定百分比概括代码体积、启动时间或内存收益。比较时记录 AGP/R8 版本、构建变体和规则,在相同设备与测试条件下检查:
| 指标 | 验证方式 |
|---|---|
| 体积 | 比较同一分发形式的 APK/AAB 或 DEX;记录代码与资源配置 |
| 启动与运行时表现 | 使用一致的启动场景、采样方法和设备 |
| 功能正确性 | 对优化后的 release 产物验证反射、JNI、序列化及动态加载路径 |
| 故障定位 | 保存与发布版本对应的 mapping.txt,用于还原混淆堆栈 |
# 注意事项
- 调试产物与经过 R8 优化的 release 产物可能表现不同,测试发布前实际使用的优化产物。
- 给动态访问补规则前,先确认真实入口;通过构建配置、产物分析和复现结果判断,避免无依据地扩大 keep 范围。
configuration.txt、usage.txt等文件是否生成及其路径取决于配置和版本。规则、产物说明见 ProGuard 记录。
# 最佳实践
调整一次配置后比较结果,保存最终规则与对应构建信息。暂时限制优化用于定位问题时,应记录具体禁用项及原因;解决问题后重新验证目标配置。
具体调研与 smali 观察见 shrink 代码减包记录,其中历史现象不能直接推广到其他 AGP/R8 版本。
# 构建产物与字节码校验
先区分:中间 JAR 不等于 R8 最终输出
build 中确实通常有 .class 和 .jar,但对于 Android 应用模块的 APK 构建,R8 通常直接生成 DEX,不保证存在一份“经过全部 R8 优化的最终 class JAR”。要证明发布代码被如何优化,主要检查 R8 输出的 DEX,再与同一次构建打包进 APK 的 DEX 对照。
简化的代码构建链路如下;实际任务会因 AGP、Kotlin 插件、变体及其他插件而变化:
Java / Kotlin / 生成的源码
→ javac / Kotlin 编译器
→ JVM .class(以目录或 JAR 承载)
→ 汇集应用、依赖及插件处理后的 class,加入规则与运行库信息
├─ minifyEnabled=true:R8 分析、缩减、优化、混淆及 DEX 生成
└─ minifyEnabled=false:通常经 D8 转换及 DEX 合并,不做 R8 全程序缩减
→ classes.dex、classes2.dex……
→ 与编译后的资源、Manifest、assets、native 库等打包、对齐、签名
→ APK
2
3
4
5
6
7
8
9
资源编译由 AAPT2 等任务处理,与代码链路有交汇,但不是“资源先转换成 class 再交给 R8”。Java 字节码的 desugaring 也属于构建过程;启用 R8 时,不应固定理解为独立的“R8 输出 JAR → D8 输出 DEX”两步。R8 官方说明 (opens new window)明确区分了 DEX 与 classfile 输出模式。
在 build 中找什么
以下为常见位置或搜索范围,不是跨版本稳定的路径契约。release 是示例变体;有 product flavor 时可能是 demoRelease 等。
| 产物 | 常见位置或查找范围 | 用途与限制 |
|---|---|---|
| Java 编译结果 | app/build/intermediates/javac/<variant>/.../classes/ | 检查 javac 输出,通常在 R8 之前 |
| Kotlin 编译结果 | app/build/tmp/kotlin-classes/<variant>/ | 检查 Kotlin 编译结果,可能有额外生成的类 |
| 中间 JAR | app/build/intermediates/ 下的 classes.jar 等 | 必须确认生成任务;可能是依赖、打包的 class,也可能只包含 Java 资源 |
| R8 的 DEX 输出 | app/build/intermediates/ 下,对应 minify<Variant>WithR8 输出的 .dex | 直接观察优化结果;检查全部 classes*.dex,不能只看主 DEX |
| R8 报告 | 通常在 app/build/outputs/mapping/<variant>/ | 用于辅助确认名称与配置,不替代字节码检查 |
| 发布 APK | 通常在 app/build/outputs/apk/ | 最终交付代码的核对对象 |
在 Android 项目根目录执行;模块名不是 app 时替换对应路径:
# 先构建目标变体;检查日志中实际的编译与 R8 任务
./gradlew :app:assembleRelease --info
# build 常被 Git 忽略,搜索时需要 --no-ignore
rg --files --hidden --no-ignore app/build \
-g '*.class' -g '*.jar' -g '*.dex' \
-g 'mapping.txt' -g 'configuration.txt' -g 'usage.txt' -g 'seeds.txt'
2
3
4
5
6
7
不要把其他变体或旧构建遗留的产物混进比较;搜索结果本身不能证明文件由当前 R8 任务生成。结合任务日志及其输出确认来源。若要做长期自动化收集,优先使用对应 AGP 版本的 Variant/Artifacts API,避免依赖内部目录;其中 ScopedArtifact.CLASSES (opens new window) 表示 class 产物,不是“最终优化 DEX”的替代物。
检查优化前的 JVM 字节码:javap
以下路径、类名均为示例,替换为搜索到的实际产物:
# 先看 JAR 内容:有 .class 才能按 JVM class 检查
jar tf path/to/classes.jar
# JAR 中的类
javap -classpath path/to/classes.jar -c -p -s com.example.OptimizeProbe
# 或直接检查 class 目录中的类
javap -classpath app/build/tmp/kotlin-classes/release \
-c -p -s com.example.OptimizeProbe
2
3
4
5
6
7
8
9
-c 显示指令,-p 包含私有成员,-s 显示类型描述符;需要常量池、注解等信息时再加 -v。javap 检查的是 JVM .class,不能拿它直接读取 DEX。JDK javap 文档 (opens new window)
这一阶段适合建立对照:方法、字段是否原本就存在,调用是否已被 javac/Kotlin 编译器改写。例如常量已在编译阶段内联,就不能把最终看不到字段访问完全归功于 R8。JAR 只是容器;其中若装的是 classes.dex,仍应使用 DEX 工具。
不反编译整个 APK,直接检查 R8 的 DEX
Android SDK Build Tools 中的 dexdump 可以反汇编 DEX 的代码段。下例的 SDK 版本、DEX 路径需要替换;-d 是反汇编,-c 只校验校验和,后者不能验证优化是否符合预期。AOSP dexdump 参数实现 (opens new window)
# 替换成实际 SDK 工具和当前 R8 任务输出的 DEX
/path/to/android-sdk/build-tools/34.0.0/dexdump \
-d path/to/r8-output/classes.dex
2
3
如果要核对打包后的发布产物,SDK Command-Line Tools 的 apkanalyzer 可以直接输出指定类的 smali,无需把整个 APK 反编译成 Java:
# 假设 apkanalyzer 已加入 PATH,APK 路径替换为实际输出
apkanalyzer dex list app/build/outputs/apk/release/app-release.apk
apkanalyzer dex packages --defined-only app/build/outputs/apk/release/app-release.apk
apkanalyzer dex code --class com.example.OptimizeProbe \
app/build/outputs/apk/release/app-release.apk
2
3
4
5
已混淆时,先查同一构建的 mapping.txt,把 --class 换成 DEX 中的实际类名。只看某个方法可再加 --method 'methodName()V',参数采用 DEX 方法描述符,不能写 Java 风格签名。apkanalyzer 官方命令说明 (opens new window)
怎样判断优化真的发生了
| 要验证的现象 | 应查看的证据 | 容易误判的情况 |
|---|---|---|
| 未使用的类或成员被删除 | 编译后的 class 中存在,最终全部 DEX 中无对应定义;结合 usage.txt | 类可能改名或合并;原名搜不到不等于删除 |
| 方法被内联 | 查看调用点及调用指令,确认逻辑是否转移进调用者 | 方法定义消失不代表功能消失,也不一定就是内联 |
| 表达式或分支被简化 | 对比编译后 JVM 指令与最终 DEX 的运算、条件跳转 | 两者指令集不同,不能逐条按文本直接比较 |
| 配置确实生效 | 检查合并的 configuration.txt、构建日志及 DEX | 只看手写规则会漏掉默认规则和依赖的 consumer rules |
可临时用 -dontobfuscate 降低名称追踪难度,但最终仍需验证真实发布配置。不要为观察一个类随手加 -keep class ... { *; }:它会限制缩减和优化,使实验条件发生变化。mapping.txt 是名称/位置追踪信息,usage.txt 是缩减报告,seeds.txt 是保留入口报告,都不是完整的优化执行日志。
固定 AGP/R8 版本、变体、输入与规则,一次改变一个条件;先检查字节码,再运行优化后的产物验证行为。反编译得到的 Java 是工具重建的表示,不能单凭其排版或语法变化证明某项 R8 优化。
确实需要优化后的 class JAR 时
R8 本身支持 --classfile,可以用于单独的 JVM 字节码实验;这不同于 Android 应用默认的 DEX 输出模式。下面假设已有指定版本的 r8.jar、完整输入、运行库和入口规则,路径均须替换:
java -cp path/to/r8.jar com.android.tools.r8.R8 \
--release --classfile \
--output output.jar \
--pg-conf proguard.cfg \
--lib path/to/runtime-library.jar \
input.jar
2
3
4
5
6
再用 jar tf output.jar 和 javap 检查输出。运行库、依赖和入口规则必须匹配目标程序;缺失依赖或没有正确入口会让实验无效。classfile 模式的结果不能等同于 Android DEX 后端的最终结果。
另一个例外是启用缩减的 Android library/AAR 构建,它可以输出处理后的 class JAR。AGP 8.4 的库类缩减说明 (opens new window)记录了库子项目发布缩减后类的行为变化。因此,判断一个 JAR 是否经过 R8,应确认它所属模块、变体与生成任务,不能只凭文件名叫 classes.jar。