系列文章《Android 工程师的 100 篇进阶笔记》第 62 篇
上一篇讲了 APK 体积整体优化,这篇专攻 R8/ProGuard 的进阶能力。除了默认压缩,还有混淆字符串、删除调试日志、Keep 规则调优这些"重武器"。
R8 vs ProGuard
Android 长期用 ProGuard(来自 GuardSquare 的商业产品),2018 年后 Google 用自家 R8 替代:
R8 默认 mode 是 compat,AGP 8.0+ 默认 fullMode:
// build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), // R8 aggressive
"proguard-rules.pro"
)
}
}
}
proguard-android-optimize.txt 比 proguard-android.txt 多一些激进优化(多次优化、合并重复代码)。
Keep 规则的本质
R8 不是简单删代码。它的规则分三层:
[1] 保留入口:哪些代码不能删
- Application 类
- Activity / Service / BroadcastReceiver / ContentProvider 子类
- JNI 调用方法
- 反射 / annotation 用到的类
[2] 优化规则:能合并/内联的尽量做
- 移除未使用代码
- 方法内联
- 字符串字段优化
[3] 混淆:改名字让代码难看懂
- 重命名类、方法、字段
- 重命名 - 不影响可读性的命名空间
实战规则集
下面是生产环境可用的完整规则集,按问题归类:
1. 模型类(保持字段名)
数据类、序列化框架用的模型类必须 keep,否则序列化失败:
# Gson
-keep class com.example.model.** { *; }
-keepattributes Signature
-keepattributes *Annotation*
# Moshi
-keep class com.example.model.** { *; }
-keepclassmembers class * {
@com.squareup.moshi.JsonClass *;
}
2. Kotlin 反射
Kotlin 的 kotlin.Metadata 注解保留:
-keepattributes RuntimeVisibleAnnotations
-keepattributes RuntimeVisibleParameterAnnotations
-keep class kotlin.Metadata { *; }
-keepattributes Signature, InnerClasses, EnclosingMethod
3. 协程
kotlinx.coroutines 用到反射调试栈:
-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler { *; }
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory { *; }
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory { *; }
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler { *; }
4. Enum
Enum 经常被反序列化或反射用:
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
5. 第三方库的官方规则
很多库自带规则:
# Retrofit
-dontwarn retrofit2.**
-keepattributes Signature, Exceptions
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
# Glide
-keep class com.bumptech.glide.** { *; }
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class * extends com.bumptech.glide.GeneratedAppGlideModuleImpl
# Hilt
-keep,allowobfuscation,allowshrinking class dagger.hilt.android.internal.managers.* { *; }
-keep,allowobfuscation,allowshrinking @dagger.hilt.android.HiltAndroidApp class *
# Room
-keep class androidx.room.RoomDatabase { *; }
-keep @androidx.room.Entity class * { *; }
6. 自定义 View
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}
7. 序列化/反序列化
Parcelable 字段:
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
R8 FullMode 注意点
getDefaultProguardFile("proguard-android-optimize.txt") 已经默认启用 fullMode 的大部分。fullMode 的关键区别:
老的程序经过 fullMode 优化经常出问题,常见:
1. 反射调用的类被改名
Class.forName("com.example.MyClass").newInstance()
R8 改名后类名找不到。解决方案:构造器加 @Keep 或写 keep 规则。
2. JNI 方法匹配失败
companion object {
init {
System.loadLibrary("native")
}
external fun nativeLogin(token: String): Int
}
R8 改 native 方法名,但 C++ 还在找旧名字。规则:
-keepclasseswithmembernames class * {
native <methods>;
}
3. 注解被剥离
R8 默认不剥离 RuntimeVisibleAnnotations,但剥离 RuntimeInvisibleAnnotations。如果用注解做权限检查或路由:
-keepattributes *Annotation*
字符串混淆
R8 默认不混淆字符串内容(只是改类名)。商业敏感(API key、URL)的字符串需要加密:
# 自己实现字符串加密
-keep,allowobfuscation class com.example.SecurityUtil {
public static java.lang.String decrypt(java.lang.String);
}
# 把字符串预先加密:
// 编译前宏:SOURCE_TOOLS_ENCRYPTION=true
// 源码中的:"https://api.example.com"
// 替换为:DecryptUtil.decrypt("EncryptedString")
开源方案:
StringFog(美团):字符串加密 Gradle 插件
Paranoid(Yandex):Java/Kotlin 类和字符串混淆
调试日志删除
Log 类调用虽然不占空间,但是泄漏业务逻辑和调试代码。用日志优化插件:
plugins {
id("id.zelory.compressor")
}
// 删除全部 Log.v Log.d Log.i 调用
// 保留 Log.w Log.e
或者自己用 ProGuard 规则:
# 删除 RLog.d 调用
-assumenosideeffects class com.example.RLog {
public static int d(...);
public static int v(...);
}
R8 识别出 d(...) 是空方法后会连调用一起删除。
自定义 R8 规则
R8 支持自定义 Java agent 改字节码,但更实用的方式是写自定义 Gradle 任务:
// buildSrc/src/main/kotlin/RuleGenerator.kt
class RuleGenerator {
@TaskAction
fun generate() {
val sb = StringBuilder()
// 扫描所有被 @RouterApi 注解的类
// 自动生成 keep 规则
outputFile.writeText(sb.toString())
}
}
不推荐自己造这种轮子,但简单项目可以用。
调试反混淆后的 Crash
Crashlytics / Bugsnag 收到的崩溃堆栈是混淆后的字段:
java.lang.NullPointerException:
at com.example.b.a(Unknown Source:42)
at com.example.c.b(Unknown Source:18)
需要 mapping.txt 反混淆:
# R8 在 build/outputs/mapping/release/mapping.txt
# 上传给 Crashlytics
Android Studio 上也能反混淆:
Run > Edit Configurations > +
> ProGuard/R8 > Mapping File Path
验证混淆成功的步骤
# 1. R8 是否报错
./gradlew :app:assembleRelease --info 2>&1 | grep -i 'r8\|keep'
# 2. 检查 mapping 文件
cat app/build/outputs/mapping/release/mapping.txt | head -50
# 3. 用 APK Analyzer 看到底多少类被删了
# -> Dex packages 标签页,对比 release vs debug 的类数
# 4. 跑 monkey 测试
adb shell monkey -p com.example --pct-syskeys 0 \
--throttle 100 1000
# 5. 跑核心流程的 Espresso 测试
./gradlew :app:connectedAndroidTest
性能 trade-off
混淆和优化是有成本的:
大型项目 release 构建要从 30s 涨到 1-2 分钟。但打包时间长换来包体积小、运行快,怎么选看场景。
小结
R8 配置表面简单实则深水区。做好 R8 既需要懂 ProGuard 语法,也要理解 Android 运行时机制。从模型类 keep、JNI 字段命名、字符串加密,到日志剥离、性能取舍,都值得花时间研究。建议每个项目都做一次 R8 配置审计,用工具扫描不合理的规则、能合并的 keep。
下一篇聊聊离线优先架构——移动端的数据存储设计。