第三章 混淆规则基础:保留类/方法/字段、通配符与条件规则

说到 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 *; } 是没用的——因为整个类都已经被保留了。

我的经验是:把精确规则写在前面,宽泛规则写在后面。这样读规则文件时,脑子能从上到下直觉地理解“先保留精准的,最后兜底”。

本章知识结构

ProGuard保留规则知识结构图 保留规则核心 通配符匹配 * 单级类名 ** 多级路径 ? 单字符 保留目标分类 类级别 成员级别 注解级别 条件保留规则 -if 前置条件 类存在判断 组合示例:-keep class * implements com.example.IService { public *; } 常见错误 1. -keep 写得太宽泛,导致无用类被保留 2. 通配符 ** 与 * 混淆,匹配范围不对 提示:先用 -whyareyoukeeping 诊断保留原因 再逐步收紧规则,避免过度保留

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 规则时,就能一眼看出哪些是多余的脏数据了。嗯,动手试试吧。