18、混淆与加固:ProGuard/R8配置、混淆规则编写、Keep规则、资源混淆、加固集成
说到混淆与加固,这可能是很多Android开发者又爱又恨的话题。爱的是它能让你的APK变小、变难破解,恨的是——嗯,一混淆就崩,一加固就闪退。我刚开始带项目那会儿,就因为在混淆配置上偷了个懒,结果线上版本疯狂报ClassNotFoundException,那叫一个惨。
说白了,混淆就是给你的代码「改个名」,让反编译的人看得一头雾水。加固呢,则是给APK穿上一层「防弹衣」,防止被直接解包。今天咱们就把这两件事彻底聊透。
核心要点:混淆不是玄学,是规则游戏。你只要搞懂「谁要保留、谁可以改」,剩下的交给ProGuard/R8就行。
18.1 ProGuard vs R8:到底用哪个?
从Android Gradle Plugin 3.4.0开始,Google默认用R8替代了ProGuard。R8不光能做混淆,还能做脱糖、资源缩减,而且速度更快。我个人习惯是直接用R8,除非遇到某些第三方库死活不兼容,才会切回ProGuard。
怎么切?很简单,在gradle.properties里加一行:
android.enableR8=false
但我建议你别轻易动这个开关。R8的兼容性已经非常好了,我这两年遇到的R8翻车案例,基本都是混淆规则没写对,跟R8本身没关系。
18.2 混淆规则编写:从零开始
混淆规则文件通常叫proguard-rules.pro,放在模块根目录。你可以在build.gradle里指定它:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
这里有个细节:getDefaultProguardFile('proguard-android-optimize.txt')是Android SDK自带的通用规则,它会帮你处理一些基础的东西,比如泛型、枚举、内部类。你只需要在proguard-rules.pro里写你自己的特殊规则。
18.2.1 基础语法速览
| 指令 | 作用 | 示例 |
|---|---|---|
-keep |
保留类和成员,不混淆、不优化 | -keep class com.example.MyClass |
-keepclassmembers |
只保留类的成员(字段、方法) | -keepclassmembers class * { @Keep *; } |
-dontwarn |
忽略警告,防止编译中断 | -dontwarn com.example.** |
-keepattributes |
保留元数据,如注解、签名 | -keepattributes Signature |
你想想看,这些规则其实就两个核心问题:哪些东西不能动?哪些警告可以忽略?搞明白这两点,混淆规则就写好了八成。
18.3 Keep规则:谁该被保留?
我总结了一个「保命清单」,每次写混淆规则都会过一遍:
- 通过反射调用的类和方法——比如Gson、FastJson、Kotlin序列化
- JNI方法——native关键字的方法名不能变
- 枚举类——枚举的values()和valueOf()是反射调用的
- 自定义View——XML布局里引用的类名不能变
- 注解——运行时注解必须保留
- 实体类/数据类——用于JSON解析的Bean
举个例子,如果你用Gson:
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.example.model.** { *; }
我曾经遇到过一个坑:某个第三方SDK的混淆规则里写了-keep class com.thirdparty.** { *; },结果它内部有个内部类没被覆盖到,导致运行时崩溃。排查了整整一天才发现是规则写得太宽泛,漏了一个子包。从那以后,我写Keep规则都习惯用**通配符,而不是*。
小技巧:用-printmapping mapping.txt输出混淆映射表。出Bug时,用这个文件把堆栈信息反解回原始类名。我每次发版都会保留这个文件,别问为什么,问就是吃过亏。
18.4 资源混淆:不只是改名字
资源混淆,说白了就是把R.id.xxx、R.layout.xxx这些资源ID改成短名字。Android原生就支持资源缩减(shrinkResources),但真正的资源混淆需要借助第三方工具,比如微信的AndResGuard。
AndResGuard的原理很简单:把资源文件的路径和文件名改成短字符串,比如res/layout/activity_main.xml变成r/l/a.xml。这样APK体积能再小个5%-10%,而且反编译后看到的资源名全是乱码。
配置方式也很直接:
apply plugin: 'AndResGuard'
andResGuard {
mappingFile = file("./resource_mapping.txt")
use7zip = true
keepRoot = false
// 保留哪些资源不混淆
keepResName = [
"R.drawable.ic_launcher",
"R.string.app_name"
]
}
注意,资源混淆有个大坑:如果你在代码里通过getIdentifier()动态获取资源ID,那这些资源必须保留原名。否则运行时会找不到资源,直接白屏。我有个朋友就因为这个原因,上线后紧急回滚了一次。
18.5 加固集成:给APK穿上铠甲
混淆只是第一步,真正要防破解还得靠加固。市面上主流的加固方案有360加固、腾讯乐固、梆梆加固等。它们的核心思路都差不多:
- 加壳:把原始DEX加密后藏起来,运行时再解密
- 反调试:检测是否被调试器附加,发现就退出
- 资源加密:对资源文件进行加密,防止直接解包
- 代码虚拟化:把关键代码转成虚拟机指令,增加逆向难度
集成方式一般有两种:
- 命令行工具:在打包完成后,用加固工具的CLI对APK进行加固
- Gradle插件:在构建流程中自动插入加固步骤
我个人更推荐第二种。举个例子,腾讯乐固的Gradle集成:
buildscript {
dependencies {
classpath 'com.tencent.android.tpns:tpns-plugin:1.0.0'
}
}
apply plugin: 'com.tencent.android.tpns'
legu {
// 加固配置
enable true
account {
username = "your_account"
password = "your_password"
}
}
这里要提醒一句:加固和混淆的顺序不能乱。一定是先混淆、再加固。如果先加固再混淆,加固壳会把混淆后的代码再包一层,导致混淆规则失效。我见过有人把顺序搞反,结果APK体积反而变大了。
注意:加固后的APK必须经过充分测试。有些加固方案会与热修复、插件化框架冲突。我建议你在测试环境先跑一遍完整的自动化测试用例,确认没问题再上线。
18.6 知识体系总览
下面这张图是我整理的混淆与加固核心流程,你可以对照着检查自己的项目配置是否完整:
从这张图你可以看到,整个流程是线性的:从原始APK开始,先做代码混淆,再做资源混淆,最后加固。每一步都有对应的工具和规则,缺一不可。
18.7 实战建议
最后,我分享几个实战中总结的经验:
- 混淆规则要「宁多勿少」——多保留几个类不会影响安全性,但少保留一个就可能崩溃
- 每次发版前跑一次混淆后的测试——用
minifyEnabled true打一个Debug包,跑一遍所有功能 - 资源混淆和加固不是必须的——如果你的App不涉及敏感数据,只做代码混淆就够了
- 加固方案要定期更新——加固厂商会不断修复漏洞,旧版本可能已经被破解
嗯,混淆与加固这块内容就讲到这里。你只要按照上面的流程走一遍,基本不会出大问题。如果真遇到奇怪的崩溃,先检查mapping文件,再检查Keep规则——八成是这两个地方出了岔子。
公众号:蓝海资料掘金营,微信deep3321