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 知识体系总览

下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个快速索引:

混淆与代码缩减 ProGuard vs R8 R8是默认工具,兼容ProGuard规则 速度更快,输出更小 规则文件编写 -keep / -keepclassmembers -dontwarn / -keepattributes mapping.txt 记录混淆前后对应关系 用于还原崩溃堆栈 Keep规则是混淆的核心 反射、JNI、序列化场景必须保留 ⚠️ 每次发版记得存档mapping.txt

7.7 实战建议

最后,分享几个我在实战中总结的经验:

  • 先跑一次Release构建,看看有没有警告。很多问题在Debug下不会暴露。
  • 不要一股脑全-keep。能用-keepclassmembers就别用-keep,保留范围越小越好。
  • 善用@Keep注解。AndroidX提供了@Keep注解,比写规则文件更直观。
  • 测试反射场景。如果代码里用了反射,务必在Release模式下跑一遍相关功能。

再次提醒:混淆不是万能的。它只能增加逆向难度,不能完全防止逆向。核心逻辑建议放到Native层或服务端。

嗯,关于混淆和代码缩减,核心内容就是这些。规则文件写起来不难,难的是想清楚哪些该保留、哪些可以混淆。多踩几次坑,你自然就熟练了。


公众号:蓝海资料掘金营,微信deep3321