7、混淆与代码缩减(ProGuard/R8)
这一章,我们来聊聊Android构建中一个绕不开的话题——代码混淆与缩减。
说白了,就是让你的APK变得更小、更难被逆向。我刚开始做Android开发那会儿,对混淆的态度就是「能跑就行」,直到有一次线上版本被反编译,核心逻辑被人抄了去……嗯,从那以后,我再也不敢轻视这步了。
7.1 为什么需要混淆与代码缩减?
你想想看,Java/Kotlin编译出来的字节码,类名、方法名都是可读的。别人拿个反编译工具,几乎能还原出你的源码逻辑。这显然不行。
混淆(Obfuscation)就是把类名、方法名、字段名改成a、b、c这种无意义的短名。代码缩减(Shrinking)则是把没用的代码、资源统统删掉。
核心目标:
- 减小APK体积(缩减无用代码)
- 增加逆向难度(混淆命名)
- 优化性能(移除无用分支)
7.2 ProGuard vs R8:到底有什么区别?
很多新手会问:ProGuard和R8,我该用哪个?
其实从Android Gradle Plugin 3.4.0开始,R8就已经是默认的混淆和缩减工具了。ProGuard是前辈,R8是后来者,但R8完全兼容ProGuard的规则文件。
| 对比项 | ProGuard | R8 |
|---|---|---|
| 维护方 | GuardSquare(第三方) | Google(内置在AGP中) |
| 速度 | 较慢 | 快2-3倍 |
| 输出大小 | 基准 | 通常更小 |
| 规则兼容性 | 原生规则 | 兼容ProGuard规则 |
| 默认启用 | 需手动配置 | AGP 3.4.0+ 默认 |
我个人习惯直接用R8,毕竟Google亲儿子,更新快、bug少。但如果你有非常复杂的ProGuard规则,R8偶尔会有兼容问题——我在项目中遇到过两次,都是因为自定义的-keepclassmembers规则写得太「野」了。
小提示:在gradle.properties中设置android.enableR8=true可以强制启用R8。不过现在新项目默认就是R8,不用操心。
7.3 ProGuard/R8规则文件编写
规则文件通常叫proguard-rules.pro,放在模块根目录。你可以在build.gradle里指定路径:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
这里getDefaultProguardFile会引入Android官方提供的默认规则,主要处理一些系统类的混淆问题。然后你自己的规则写在proguard-rules.pro里。
7.3.1 基础语法速览
规则文件其实不复杂,记住几个核心指令就行:
-keep:保留类和成员,不混淆、不缩减-keepclassmembers:只保留类的成员(方法、字段),类名可以混淆-dontwarn:忽略某些警告-keepattributes:保留某些属性,比如注解、泛型-assumenosideeffects:假设某些方法没有副作用,直接移除调用
举个例子:
# 保留整个类
-keep class com.example.MyClass { *; }
# 保留某个类的所有public方法
-keepclassmembers class com.example.MyClass {
public <methods>;
}
# 保留使用了@Keep注解的类
-keep @androidx.annotation.Keep class * { *; }
# 忽略某个库的警告
-dontwarn com.thirdparty.library.**
注意:-keep写得太宽泛会导致混淆效果大打折扣。我曾经接手过一个项目,整个com.example包都被-keep了,APK体积大了30%……
7.4 Keep规则详解
Keep规则是混淆中最容易出错的地方。我总结了几种常见场景:
7.4.1 保留反射调用的类
反射是混淆的头号杀手。如果你在代码里通过字符串获取类名,比如Class.forName("com.example.MyClass"),那这个类必须保留。
-keep class com.example.MyClass { *; }
7.4.2 保留JNI方法
JNI方法名必须和C/C++代码一致,不能混淆。
-keepclasseswithmembernames class * {
native <methods>;
}
7.4.3 保留Parcelable实现
Parcelable依赖CREATOR字段,混淆后会导致反序列化失败。
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
7.4.4 保留枚举
枚举的values()和valueOf()是编译器生成的,混淆后会出问题。
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
避坑指南:我曾经因为没保留枚举的values()方法,导致一个支付状态机在Release版中直接崩溃。排查了整整一天……从那以后,枚举的keep规则我都是直接复制粘贴的。
7.5 混淆映射文件(mapping.txt)
混淆之后,你的堆栈信息就变成了a.a.b()这种鬼样子。这时候就需要mapping.txt来还原。
每次Release构建,R8/ProGuard都会生成一个mapping.txt文件,位置在:
app/build/outputs/mapping/release/mapping.txt
这个文件记录了原始类名、方法名与混淆后的名字的对应关系。比如:
com.example.MyActivity -> com.example.a:
void onCreate(android.os.Bundle) -> a
void onResume() -> b
当线上出现崩溃时,你可以用retrace工具(或者Android Studio自带的「Analyze Stack Trace」功能)把混淆后的堆栈还原成可读的。
# 使用retrace命令
retrace.bat -verbose mapping.txt obfuscated_stacktrace.txt
我的习惯:每次发版后,我都会把mapping.txt按版本号存档,比如mapping_v2.3.1.txt。不然等三个月后线上出问题,你根本找不到对应版本的映射文件。
7.6 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个快速索引:
7.7 实战建议
最后,分享几个我在实战中总结的经验:
- 先跑一次Release构建,看看有没有警告。很多问题在Debug下不会暴露。
- 不要一股脑全-keep。能用
-keepclassmembers就别用-keep,保留范围越小越好。 - 善用
@Keep注解。AndroidX提供了@Keep注解,比写规则文件更直观。 - 测试反射场景。如果代码里用了反射,务必在Release模式下跑一遍相关功能。
再次提醒:混淆不是万能的。它只能增加逆向难度,不能完全防止逆向。核心逻辑建议放到Native层或服务端。
嗯,关于混淆和代码缩减,核心内容就是这些。规则文件写起来不难,难的是想清楚哪些该保留、哪些可以混淆。多踩几次坑,你自然就熟练了。