11、Android资源与代码混淆:minifyEnabled、proguardFiles、shrinkResources、自定义混淆规则、混淆映射文件
说到混淆,很多刚入行的朋友会觉得这玩意儿离自己很远。其实不然。我刚开始带项目那会儿,就吃过没开混淆的亏——APK包体直接飙到30多兆,里面还躺着各种第三方库的完整类名,随便一个反编译工具就能把代码逻辑扒个精光。嗯,从那以后,混淆就成了我每个项目的标配。
这一章,咱们就把Android混淆这件事儿彻底聊透。说白了,混淆就是两件事:让代码变小,让代码变难懂。前者靠ProGuard/R8,后者靠资源压缩。咱们一个一个来。
11.1 minifyEnabled:混淆的总开关
先看最基础的配置。在build.gradle里,你会看到这样一个属性:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
minifyEnabled就是混淆的总开关。设为true,Gradle就会在构建时启动代码混淆和优化。设为false,那下面所有配置都白搭。
你可能会问:为什么release才开?因为混淆会重命名类名、方法名,debug时堆栈信息全是a、b、c,你根本看不懂。所以debug保持原样,release再压缩混淆,这是行业惯例。
11.2 proguardFiles:规则文件从哪来
proguardFiles指定了混淆规则文件。这里有两个文件:
- getDefaultProguardFile('proguard-android-optimize.txt'):Android SDK自带的默认规则。它位于
ANDROID_HOME/tools/proguard/目录下。这个文件包含了Android项目通用的混淆规则,比如不混淆四大组件、不混淆View的构造方法等。 - proguard-rules.pro:项目自定义规则。你需要在app模块下创建这个文件,把项目特有的混淆规则写进去。
我个人建议:永远不要修改默认规则文件。你只需要在proguard-rules.pro里追加自己的规则就行。这样升级SDK时不会出问题。
11.3 shrinkResources:资源压缩的利器
代码混淆完了,资源文件呢?很多APK里躺着大量未使用的图片、布局、字符串,白白占空间。shrinkResources就是干这个的。
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
}
}
注意:shrinkResources必须配合minifyEnabled true使用。因为资源压缩需要先知道哪些代码还在用,哪些已经删了——这信息来自代码混淆阶段。
shrinkResources设为true,结果运行时崩溃了。查了半天才发现,有个第三方库通过反射加载资源ID,资源被误删了。解决方案是在res/raw/keep.xml里用tools:keep保留特定资源。
具体写法如下:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/my_layout,@drawable/ic_launcher*" />
你想想看,如果不用shrinkResources,那些无用的资源就会一直躺在APK里。对于大型项目,这能省下好几兆的空间。
11.4 自定义混淆规则:保护该保护的
混淆不是万能的。有些代码必须保留原名,否则运行时就会出问题。比如:
- 通过反射调用的类和方法
- JNI方法
- JSON/XML序列化的数据类
- 第三方SDK的入口类
这些都需要在proguard-rules.pro里手动指定保留规则。常用的规则有:
# 保留某个类
-keep class com.example.MyClass { *; }
# 保留某个包下的所有类
-keep class com.example.model.** { *; }
# 保留某个类的特定成员
-keep class com.example.User {
public <init>(...);
public java.lang.String getName();
}
# 保留注解
-keepattributes *Annotation*
# 保留泛型信息(用于Gson等)
-keepattributes Signature
# 保留枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
这里有个小技巧:不要一股脑全保留。我见过有人直接写-keep class ** { *; },那混淆等于没开。正确的做法是:只保留那些运行时必须的,其他的交给ProGuard处理。
11.5 混淆映射文件:崩溃日志的救命稻草
混淆之后,类名变成了a、b、c。如果线上崩溃了,堆栈信息全是a.a()、b.b(),你根本不知道是哪行代码出了问题。这时候就需要混淆映射文件。
映射文件默认生成在app/build/outputs/mapping/release/mapping.txt。它的内容长这样:
com.example.MainActivity -> com.example.a:
void onCreate(android.os.Bundle) -> a
void initView() -> b
com.example.User -> com.example.b:
java.lang.String name -> a
int age -> b
左边是原始类名/方法名,右边是混淆后的名字。有了这个文件,你就可以用retrace工具把混淆后的堆栈还原成可读的格式。
使用方式:
# 在Android SDK目录下
retrace.bat -verbose mapping.txt stacktrace.txt
或者直接在Android Studio里:点击菜单栏的Build > Analyze Stack Trace,粘贴混淆后的堆栈,选择对应的mapping文件,一键还原。
mapping_v2.1.3.txt。这样线上出问题,几分钟就能定位到具体代码。
11.6 知识体系总览
说了这么多,咱们用一张图把整个混淆流程串起来:
这张图把整个流程讲得很清楚了。从源代码开始,先判断是否开启混淆。不开就直接打包,开了就进入ProGuard/R8处理,同时加载规则文件。然后进行资源压缩,最后输出混淆后的APK和映射文件。
11.7 实战建议
最后,我总结几条实战经验:
- 先开混淆,再开资源压缩。顺序不能乱,否则资源压缩会误删。
- 每次发布都保存mapping.txt。这是线上排查问题的唯一线索。
- 用R8替代ProGuard。从AGP 3.4开始,R8是默认的混淆工具,它比ProGuard更快、优化更好。
- 测试混淆后的包。我习惯在提测前,专门跑一遍混淆后的回归测试,确保所有功能正常。
- 不要迷信默认规则。默认规则只覆盖了通用场景,你的项目特有的反射、序列化、JNI调用,都得自己加规则。
嗯,混淆这块儿其实不难,但细节很多。你只要记住:能混淆的尽量混淆,不能混淆的精确保留,再配合映射文件,线上问题就能快速定位。这一章的内容就到这里,下一章咱们聊聊构建变体,也就是Build Variants的那些事儿。