如何防止某些类或方法被 R8 移除或混淆?
解读
国内面试中,面试官问“如何防止被 R8 移除/混淆”通常不是想听一句“加 keep 就行”,而是考察你对 R8 与 ProGuard 差异、AGP 打包流程、反射/JNI/序列化场景、以及 Gradle 配置细节的系统性理解。回答时要体现“知道它为什么会被删、知道在哪一层拦截、知道不同业务场景该用哪一招”,否则很容易被追问“keep 规则写在哪?debug 生效吗?compose 还能用 proguard-rules.pro 吗?”而露馅。
知识点
- R8 与 ProGuard 的核心差异:R8 是 D8 的继任者,集成压缩、优化、混淆、脱糖四步,默认开启;ProGuard 仅做优化与混淆,AGP 7.0+ 默认强制 R8。
- 入口规则(-keep 系列)与“可达性”算法:R8 以入口为根做可达性分析,未到达即被删。
- keep 指令粒度:-keep 保留类名、方法体;-keepclassmembers 仅保留成员;-keepclasseswithmembernames 满足条件才保留;混淆映射用 -keepnames。
- 注解驱动:@Keep 为 AGP 内置注解,编译期自动生成等效 -keep,源码级最直观;但仅对当前模块生效,library 模块需主动依赖 androidx.annotation。
- 多模块规则合并顺序:consumerProguardFiles(library 的 consumer-rules.pro) > 主模块 proguard-rules.pro > 默认 AAPT 生成规则 > R8 内置规则;debug 默认关闭混淆,但 minifyEnabled true 即生效。
- 反射、JNI、Gson、Serializable、aidl、ViewBinding、DataBinding、Navigation Args、Hilt、Room 实体、WorkManager 的默认调度器、Compose 编译器生成的 “Composable” 签名,都是高频“误删”场景,需要针对性 keep。
- 国内多渠道(华为、小米、OPK、应用宝)二次加固时,加固工具会再跑一次混淆,若 mapping.txt 未对齐,会出现“方法找不到”崩溃,因此 CI 需归档 mapping 并校验加固前后一致性。
- Gradle 配置陷阱:
- debuggable true 时若 minifyEnabled true,R8 仍会压缩,但调试信息被剥离,断点行号对不上;
- shrinkResources true 依赖 minifyEnabled,且只删资源不删代码;
- useProguard true 可回退到 ProGuard,但 AGP 8.0 已废弃。
- 验证手段:./gradlew assembleRelease -Pandroid.enableR8.fullMode=true --info 查看 “Removed unused classes:” 日志;使用 retrace 工具对 mapping.txt 还原线上堆栈;CI 集成 “r8-integration-tests” 做回归。
答案
线上崩溃 80% 源于“被 R8 误删”,防止手段分四层:
- 源码层:给被反射或 JNI 调用的类/方法加 @Keep,library 模块在 consumer-rules.pro 中 -keep 对应成员;Compose 函数用 -keep,allowobfuscation @interface androidx.compose.runtime.Composable { *; } 防止签名被裁。
- 规则层:主模块 proguard-rules.pro 按场景细化:
Gson 实体
-keepclassmembers,allowobfuscation class ** { @com.google.gson.annotations.SerializedName <fields>; }JNI
-keepclasseswithmembernames class * { native <methods>; }ViewBinding/BR
-keep class .databinding. { *; }Room 实体与 DAO
-keep @androidx.room.Entity class * { *; }
-keep @androidx.room.Dao interface * { *; } - 构建层:release 包必开 minifyEnabled=true、shrinkResources=true,同时把 mapping.txt 归档到版本仓库;CI 脚本中通过 r8-integration-tests 跑一遍 “assembleReleaseTest” 验证反射调用无 NoSuchMethodError。
- 加固层:国内渠道加固前,把原始 mapping.txt 传入加固平台,关闭其二次混淆开关;若平台强制混淆,需上传 -applymapping mapping.txt 保证符号一致。
一句话总结:先让 R8 “看见”入口(@Keep 或 -keep),再按“反射/JNI/序列化”场景写最小粒度规则,最后通过 CI 验证与加固对齐,才能既瘦身又稳态。
拓展思考
- 全模式 R8(fullMode=true)会把常量内联、类合并做到极致,导致 Gson 的字段名、Kotlin 的 companion object 被彻底抹掉;此时需用 -keepattributes Signature,InnerClasses,EnclosingMethod 并开启 @SerializedName 全覆盖,或迁移到 Kotlin 序列化。
- Jetpack Compose 编译器生成带 “” 的 Composable 函数,若被 Navigation for Compose 用反射实例化,需额外 -keepclassmembers class ** { ****(androidx.compose.runtime.Composer,...); },否则 1.5.0 以后会出现 “IllegalArgumentException: Unable to instantiate NavGraph” 。
- 动态下发插件(RePlugin、Shadow)宿主与插件共用接口时,接口包名必须固定,建议把接口独立成 android-library,consumer-rules.pro 中 -keep 接口全限定名,并在宿主构建时通过 “applymapping” 强制对齐,防止插件调用出现 AbstractMethodError。
- 大型项目可自定义 R8 配置规则生成器:在编译期扫描所有 @Keep、@SerializedName、@Entity、@Dao、@Composable 注解,结合 ASM 解析 JNI 方法,自动生成 keep-rules.pro,彻底告别“人工漏写”导致的线上事故。