ProGuard基础规则:核心指令详解
说实话,很多人刚接触混淆时,看到那一堆 -keep 就头疼。我刚开始也是这样,总觉得「我全都要 keep 住不就完了吗」——但后来发现,那等于没混淆。正确理解这几个核心指令,是你用好 ProGuard 的第一步。
1. -keep:谁也别动我
-keep 是最常用的指令,意思就是:保留指定类和成员,不混淆、不收缩、不优化。
我个人习惯把它分成三类场景:
- 保留整个类:比如你的 Activity 或者 Fragment,被 AndroidManifest 引用的类必须保留。
- 保留类中特定成员:比如只保留 public 方法,其他仍可混淆。
- 保留类中所有成员:用
-keep class * { *; }这种通配符。
// 保留 com.example.MyActivity 整个类
-keep class com.example.MyActivity
// 保留 com.example 包下所有类的所有成员
-keep class com.example.** { *; }
// 保留指定类的 public 方法
-keep class com.example.Utils {
public <methods>;
}
** 表示匹配任意子包,* 只匹配当前包。我经常用 ** 来一把梭,但要注意性能——规则多了编译会变慢。
我在项目中遇到过一个问题:混淆后某个第三方 SDK 抛异常说「ClassNotFoundException」,排查半天发现是规则写得太宽泛,把 SDK 的内部类也给 keep 了。规则不是越少越好,而是要精准。
2. -dontwarn:闭嘴,我知道可能有问题
-dontwarn 的意思是:遇到找不到的引用时,别警告我。
为什么会需要这个?因为你可能引用了某个库,但运行时其实用不到。比如你用了 Optional 但只在 API 24 以上才存在;或者混淆后某个反射路径断了。这时 ProGuard 会报 warning,甚至中断构建。
我曾经在一个项目中把 -dontwarn 用得太多,结果漏了一个真正的类缺失,上线后闪退。嗯,这里要注意:-dontwarn 是「我知道可能有风险,但我暂时不管」,不是「所有 warning 都无视」。
// 忽略 com.example.legacy 包下所有警告
-dontwarn com.example.legacy.**
// 忽略某个具体类
-dontwarn javax.annotation.Nullable
-dontwarn 不加任何限定。至少加上包名或类名,这样出问题时还能定位。
3. -keepclassmembers:只保成员,类名该混淆就混淆
这个指令很有趣:它只保留类的指定成员,但类名本身可以混淆。举个例子,某个类通过反射调用了它的某个方法,但类名无所谓(比如通过接口调用),这时用 -keepclassmembers 就够了。
我一般把它用在:
- 枚举的 values() 和 valueOf():这些是反射调用的,不保会报错。
- ViewBinding 或 DataBinding 的 binding 类:只保留内部静态方法。
- 实现了 Parcelable 的类:CREATOR 字段必须保留。
// 保留所有枚举的 values() 方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
// 保留 Parcelable.CREATOR
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
你想想看,如果全用 -keep 会怎样?类名不混淆,攻击者一眼就能看到你的核心逻辑类。所以 -keepclassmembers 是个折中方案:保留必要的能力,但隐藏类名。
4. -keepattributes:别丢掉注解和签名
混淆有时会删除类、方法上的注解,甚至泛型签名。如果你的代码或者第三方库依赖这些信息(比如 Retrofit 的 @GET 注解、Gson 的 @SerializedName),就需要用 -keepattributes 保留。
我习惯把这几个直接写在通用规则里:
-keepattributes Signature // 保留泛型签名
-keepattributes *Annotation* // 保留所有注解
-keepattributes InnerClasses // 保留内部类关系
-keepattributes EnclosingMethod // 保留匿名内部类的方法引用
Signature 的话,Gson 或 Jackson 反序列化时会拿不到泛型信息,直接报错。我曾经踩过这个坑,调试了半天才发现是规则漏了。
5. -keepclasswithmembers:有条件地保留整个类
这个指令比较冷门,但很实用:如果类中拥有指定的成员,才保留整个类。你可以把它理解成「条件保留」。比如某个类实现了 Serializable 并且有 serialVersionUID 字段,那就整类保留;否则让 ProGuard 自己决定。
实际项目中,我一般在处理继承关系时用到它。比如基类已经 keep 了,子类如果保留某些特定方法,用这个指令可以避免手动枚举子类列表。
// 如果类中有 public static final int VERSION,则保留整个类
-keepclasswithmembers class com.example.** {
public static final int VERSION;
}
指令对比速查表
| 指令 | 类名是否混淆 | 成员是否混淆 | 典型用途 |
|---|---|---|---|
| -keep | 否(保留) | 否(保留) | Activity、Service、Application |
| -keepclassmembers | 可以混淆 | 否(仅保留指定成员) | 枚举的 values()、Parcelable.CREATOR |
| -keepclasswithmembers | 有条件保留 | 有条件保留 | 实现特定接口的类 |
| -keepattributes | 不影响 | 保留注解/签名等元信息 | Retrofit、Gson、Lambda 表达式 |
| -dontwarn | 不影响 | 不影响 | 抑制缺失引用警告 |
-dontwarn 解决编译报错,再逐条加 -keep 保证运行不崩,最后用 -keepclassmembers 做精细控制。顺序很重要。
以上就是 ProGuard 最核心的 5 条基础指令。我个人习惯在项目根目录放一个 proguard-rules.pro,按模块分块写规则,每块开头加注释说明用途。这样半年后再看,还能一眼知道当初为什么这么写。