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.xxxR.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加密后藏起来,运行时再解密
  • 反调试:检测是否被调试器附加,发现就退出
  • 资源加密:对资源文件进行加密,防止直接解包
  • 代码虚拟化:把关键代码转成虚拟机指令,增加逆向难度

集成方式一般有两种:

  1. 命令行工具:在打包完成后,用加固工具的CLI对APK进行加固
  2. 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 知识体系总览

下面这张图是我整理的混淆与加固核心流程,你可以对照着检查自己的项目配置是否完整:

混淆与加固核心流程 Release APK(未混淆) 代码混淆(ProGuard / R8) -keep规则 · -dontwarn · mapping文件 资源混淆(AndResGuard) 资源ID缩短 · 7zip压缩 · keepResName 加固(360 / 乐固 / 梆梆) 加壳 · 反调试 · 资源加密 · 代码虚拟化 加固后APK(发布) 注意事项 1. 先混淆后加固 2. 保留反射调用类 3. 保留JNI方法名 4. 保留枚举/注解 5. 保留动态资源ID 6. 保留mapping文件 7. 加固后全量测试

从这张图你可以看到,整个流程是线性的:从原始APK开始,先做代码混淆,再做资源混淆,最后加固。每一步都有对应的工具和规则,缺一不可。

18.7 实战建议

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

  • 混淆规则要「宁多勿少」——多保留几个类不会影响安全性,但少保留一个就可能崩溃
  • 每次发版前跑一次混淆后的测试——用minifyEnabled true打一个Debug包,跑一遍所有功能
  • 资源混淆和加固不是必须的——如果你的App不涉及敏感数据,只做代码混淆就够了
  • 加固方案要定期更新——加固厂商会不断修复漏洞,旧版本可能已经被破解

嗯,混淆与加固这块内容就讲到这里。你只要按照上面的流程走一遍,基本不会出大问题。如果真遇到奇怪的崩溃,先检查mapping文件,再检查Keep规则——八成是这两个地方出了岔子。


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