一、概念
1.1 代码混淆
混淆(Obfuscation)是应用发布前必不可少的环节。它不仅能缩小 APK 体积,还能增加反编译的难度,保护核心代码逻辑。
1.2 Android ProGuard / R8 流程
|--------------------|------------------------------------------|
| 压缩 Shrinking | 检测并删除未使用的类、字段、方法和属性。 |
| 优化 Optimization | 分析并优化字节码,甚至内联方法。 |
| 混淆 Obfuscation | 将类名、方法名、字段名重命名为无意义的短字符(如 a, b, c)。 |
| 预检 Preverification | 在 Java 平台上对类进行预验证。 |
1.3 不能混淆的内容
|-------------------|--------------------------------------------------------------------------------------------------------------|
| 反射使用的代码 | 反射通过字符串寻找类/方法,混淆后名称变了,反射会直接报错。 |
| 与 JS 交互的接口 | 在 WebView 中通过 @JavascriptInterface 暴露给 JS 调用的方法。 |
| 与 JS 交互的接口 | -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } |
| JNI 调用(Native 方法) | Java 层声明的 native 方法,需要与 C/C++ 层的函数名对应。 |
| JNI 调用(Native 方法) | -keepclasseswithmembernames class * { native <methods>; } |
| 序列化对象 | 实现 Parcelable 或 Serializable 接口的类,其成员名会被反射读写,不能混淆。 |
| 序列化对象 | -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; } |
| View及子类 | 在布局 XML 中引用的自定义 View,可能通过反射调用其构造方法。 |
| View及子类 | -keep public class * extends android.view.View |
| 四大组件 | 在 AndroidManifest.xml 中注册的类(系统默认已处理,通常无需手动配置)。 |
| 四大组件 | -keep public class * extends android.app.Activity -keep public class * extends android.app.Service |
| JSON 映射类(Bean) | Gson、FastJson 等库通过反射将 JSON 字符串映射为对象,类名和字段名都不能混淆。 |
| JSON 映射类(Bean) | -keep class com.yourpackage.model.** { *; } (将包名替换为您自己的) |
| 注解 | 许多库(如 EventBus、Room)使用注解,这些注解本身需要保留。 |
| 注解 | -keepattributes *Annotation* |
| 枚举 | 枚举的 values() 和 valueOf() 方法是编译器生成的静态方法,通过反射调用。 |
| 枚举 | -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } |
1.4 最佳实践建议
|---------|-------------------------------------------------------------------------------------|
| 模块化混淆 | 如果是开发 Library(SDK),请使用 consumerProguardFiles。这样集成你 SDK 的 App 会自动应用这些混淆规则,无需开发者手动复制。 |
| 尽早测试 | 不要等发布正式版前才开启混淆。建议在 debug 模式下也偶尔开启混淆进行回归测试。 |
| 善用三方库规则 | 现在的流行库(如 Retrofit, OkHttp, Glide)通常在官网或 README 中提供了成熟的混淆规则,直接复制即可。 |
二、开启混淆
在项目的 build.gradle 中针对 release 构建开启。完成配置后执行 release 构建(例如通过 ./gradlew assembleRelease),R8 就会自动运行。
Kotlin
android {
buildTypes {
release {
// 开启代码压缩、混淆和优化
minifyEnabled = true
// 开启资源压缩(删除无用图片、布局等),需配合混淆使用
shrinkResources = true
// 指定混淆规则文件
// 参数一:引用 Android SDK 提供的默认优化规则文件
// 参数二:自定义规则文件,用于添加特定于自己应用的保留规则
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
三、配置语法
R8 完全兼容 ProGuard 的配置语法,规则都写在 proguard-rules.pro 文件中 。混淆的核心是 保持(keep)那些 R8 无法通过静态代码分析自动识别为入口点的代码。错误的规则会导致应用运行时崩溃。
大多数成熟的第三方库(如 Retrofit, OkHttp, Glide 等)会在其 aar 或 jar 包中包含自己的 Proguard/R8 规则,通常不需要手动添加 。但如果遇到因混淆导致的库功能异常,可以查阅该库的官方文档,通常会提供需要手动添加的规则。
3.1 常用 keep 指令
|-------------------------|-------------------------------------|
| -keep | 保持类和类成员不被混淆或移除。 |
| -keepclassmembers | 保持成员不被混淆,但类名可以被混淆。 |
| -keepclasseswithmembers | 如果类包含指定成员,则保持类和成员都不被混淆。 |
| -dontwarn | 让 R8 忽略对某个类的找不到等警告,常用于引入的第三方库不完整时 。 |
3.2 通配符
|-------------|-------------------------|
| * | 匹配任意字符,但不包括包分隔符。 |
| ** | 匹配任意字符,包括包分隔符(匹配包及其子包)。 |
| <methods> | 匹配所有方法。 |
| <fields> | 匹配所有字段。 |
四、构建后的问题处理排查
混淆后,R8 会在 app/build/outputs/mapping/release/ 目录下生成几个关键文件:
|-------------|----------------------------------------------------------------------|
| mapping.txt | 最重要的文件。它记录了原始类、方法、字段名与混淆后名称的映射关系。请务必为每个发布的版本妥善保存此文件,它是解读混淆后崩溃日志的钥匙 。 |
| usage.txt | 列出了 R8 从 APK 中移除的、未被使用的代码。 |
| seeds.txt | 列出了所有通过 -keep 规则保留下来的代码"根"。 |
4.1 混淆后的崩溃日志难以定位
当您的应用在线上崩溃,收集到的堆栈日志是混淆过的,如 a.b.c(SourceFile:1)。
- 保留行号信息:在规则中加入 -keepattributes SourceFile,LineNumberTable。
- Android Studio 自带的 GUI 工具(点击菜单栏 Tools → Android → Analyze Stack Trace...)来直接粘贴日志和 mapping 文件进行还原 。
- 使用 Android SDK 提供的 retrace 工具(位于 sdk/tools/proguard/bin/ 下,Windows 是 retrace.bat,Mac/Linux 是 retrace.sh)和对应的 mapping.txt 文件,将混淆日志还原为可读的堆栈信息 。
XML
retrace.sh -verbose mapping.txt obfuscated_stacktrace.txt
4.2 Gson 解析结果全为 null
Debug 模式正常,Release 模式下解析后的对象字段全为 null。原因是 Bean 类的字段名被混淆,导致与 JSON 键名匹配失败。
- 给 Bean 类字段添加 @SerializedName("key") 注解。
- 或者将 Bean 类整体 keep 掉。
4.3 枚举类型报错
混淆后,Enum.valueOf() 抛出异常。
Groovy
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
4.4 依赖库重复定义
多个 jar 包包含相同的类,混淆时报错。
- 使用
-dontnote或-dontwarn压制特定包的警告,或者在configurations中排除重复依赖。
4.5 资源压缩删除了动态获取的资源
使用 getIdentifier() 动态获取资源 ID 时(如 getResources().getIdentifier("icon_" + index, "drawable", getPackageName())),资源被 shrinkResources 删除了。
- 在 res/raw 文件夹下创建 keep.xml。
XML
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/icon_*, @layout/unused_but_needed" />
4.6 桥接的某些js函数在release模式下报错
代码在 debug 模式下正常, 打包 release 包无法执行,报错"TypeError: window.JavaScriptInterface.close is not a function"。
- 在 Debug 模式下,代码不进行混淆,方法名保持为 close。 在 Release 模式下,为了减小包体积和安全性,混淆器会将 close(String s) 重命名为类似 a(String b)。由于 JavaScript 端仍然在尝试调用 window.android.close(),自然会提示找不到方法。在混淆文件中,添加如下规则:
XML
//在混淆文件中,添加如下规则
# 保持带有 JavascriptInterface 注解的方法不被混淆
-keepattributes JavascriptInterface
-keepattributes *Annotation*
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}