第三章 混淆规则基础:保留类/方法/字段、通配符与条件规则
说到 ProGuard 的混淆规则,我见过太多同学一上来就扔个 -keep 然后就不管了。嗯,结果呢?崩溃率直线飙升。我个人习惯是把规则拆成三块来理解:保留目标、通配符匹配、条件约束。说白了,这就是一套“告诉 ProGuard 哪些东西别动”的指令集。
3.1 保留规则的核心语法
ProGuard 的保留规则统一以 -keep 开头,后面跟着修饰符、类名、成员描述。我经常在项目中用这个模板:
-keep [修饰符] class 类名 {
成员规则;
}
表格里列了最常见的几种保留指令,你写规则时对照着选就行:
| 指令 | 作用 | 典型场景 |
|---|---|---|
-keep | 保留类和成员,不混淆也不删除 | 反射调用的类、JSON/XML 映射 bean |
-keepclassmembers | 只保留类的成员,类名可以混淆 | 注解处理器生成的成员 |
-keepnames | 保留类名/成员名,但允许删除未使用项 | 保持 API 接口名不变 |
-keepclassmembernames | 只保留成员名称,类名可以被混淆 | 保持 getter/setter 命名 |
-keep,allowshrinking | 保留类/成员,但允许删除未使用的 | 按需保留第三方库的入口 |
-keepclassmembers 就尽量不用 -keep。为什么?因为保留越少,混淆效果越好。比如你只需要保留某个类的某个方法,就别把整个类锁死。
3.2 类级别的保留规则
先看最简单的 —— 保留整个类。直接写全限定类名就行:
-keep class com.example.MyClass
这种写法会保留 MyClass 这个类以及它所有的方法和字段(包括私有的)。我在项目里对某些第三方 SDK 的入口类就经常这么干 —— 你不保留,运行时就给你抛 ClassNotFoundException。
如果需要保留一个包下的所有类,可以这样:
-keep class com.example.** { *; }
** 代表任意包路径(包括子包),{ *; } 代表保留所有成员。这个写法要慎用,因为它会锁死整个包。我曾经在某个大型项目里就因为这么写,导致混淆后的 APK 体积大了 3MB……嗯,后来改成按需保留了。
3.3 成员级别的保留规则
只保留类里的某些方法或字段,用成员描述符来指定。常见的匹配模式:
-keep class com.example.MyClass {
<init>(); // 保留所有构造方法
public <methods>; // 保留所有公有方法
public *; // 保留所有公有成员(方法+字段)
private int myField; // 保留指定字段
void set*(*); // 保留以 set 开头的、参数任意的方法
static ** get*(); // 保留以 get 开头的、返回值任意的静态方法
}
<init> 代表构造器,<methods> 代表所有方法,<fields> 代表所有字段。注意尖括号不能少,我第一次写时漏了它们,结果死活编译不过……
3.4 通配符的精髓
通配符是规则里最容易出错的地方,但也是威力最大的。ProGuard 提供三个通配符:
*—— 匹配任意单个字符(不包括包分隔符.)**—— 匹配任意多个字符(包括包分隔符)?—— 匹配单个任意字符(极少用到,但有时能救命)
举个例子:
// 匹配 com.example.a、com.example.b,但不匹配 com.example.ab
-keep class com.example.*
// 匹配 com.example 及其所有子包下的类
-keep class com.example.**
// 匹配 com.example.MyClass、com.example.MyClazz(最后一位任意)
-keep class com.example.MyCla??
通配符之间也能嵌套使用,比如我只想保留某个接口的实现类:
-keep class * implements com.example.ICallback
* 匹配任意类名,后面跟着 implements 指定接口。这种写法在保持回调接口时特别实用。我试过用 ** 代替 * 会怎样?会连接口自己都一起保留,有时候反而把不想暴露的内部类也暴露了。
3.5 条件保留规则
有时候我们只想在满足某些条件时才保留某个类。ProGuard 提供了 -if 条件指令:
-if class com.example.FeatureEnabled
-keep class com.example.FeatureLogic
意思是:如果 FeatureEnabled 这个类存在,就保留 FeatureLogic。常见于按功能模块裁剪的场景。
也支持更复杂的条件组合,比如:
-if class ** implements IModule
-keep class * implements IModule { *; }
这段规则的意思是:如果有任何一个实现 IModule 的类存在,那么保留所有实现 IModule 的类及其成员。这在插件化架构里经常用到 —— 你不确定用户会安装哪些模块,但必须让所有已存在的模块能正常反射调用。
-if 和 -keep 写反了顺序,导致 ProGuard 报错说找不到条件类。记住:-if 必须在 -keep 之前,而且它只影响紧随其后的那一条 -keep 规则。
3.6 规则优先级与覆盖
多条规则同时命中同一个类时,ProGuard 会按“最宽松”的原则处理。但这里有个坑:-keep 优先级高于 -keepclassmembers。如果你先写了一条 -keep class A 把整个类锁死,后面再写 -keepclassmembers class A { private *; } 是没用的——因为整个类都已经被保留了。
我的经验是:把精确规则写在前面,宽泛规则写在后面。这样读规则文件时,脑子能从上到下直觉地理解“先保留精准的,最后兜底”。
本章知识结构
3.7 实战中的规则诊断
写规则时最怕“该留的没留,该去的没去”。ProGuard 提供了一个救命工具:-whyareyoukeeping。你在规则文件里加上:
-whyareyoukeeping class com.example.MyReflectionClass
编译时 ProGuard 就会输出一条日志,告诉你这个类为什么被保留了。比如:
com.example.MyReflectionClass is kept by:
-keep class com.example.** { *; } (proguard-rules.pro:12)
看到第 12 行那个通配规则了吧?这就是典型“用力过猛”。我每次写完规则都会跑一次 -whyareyoukeeping 检查,尤其是那些不该出现的类。
“最小的保留范围 + 最精确的匹配(通配符) + 最必要的条件 ← 才能让混淆既安全又有效。”
好了,这一章的关键内容就是这些。记住:规则不是写得多就好,而是每一条你都能说出“为什么这行必须写”。下次你看到别人扔给你一坨几百行的 keep 规则时,就能一眼看出哪些是多余的脏数据了。嗯,动手试试吧。