ProGuard基础规则:核心指令详解

说实话,很多人刚接触混淆时,看到那一堆 -keep 就头疼。我刚开始也是这样,总觉得「我全都要 keep 住不就完了吗」——但后来发现,那等于没混淆。正确理解这几个核心指令,是你用好 ProGuard 的第一步。

本章核心:掌握 -keep、-dontwarn、-keepclassmembers 等 5 条基础指令,能自己写出不翻车的混淆规则。

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 基础指令决策流程图 展示从需求出发选择合适指令的决策路径 ProGuard 基础指令决策流程 需要保留什么? 整个类不能动 -keep class ... 保留特定成员 -keepclassmembers 满足条件才保留 -keepclasswithmembers 保留注解/签名 -keepattributes 抑制缺失引用警告 -dontwarn 💡 决策逻辑 先确定需求 → 再选择指令 → 最后根据编译报错补充 -dontwarn 和 -keepattributes 核心指令只有 5 条,搭配使用可以覆盖 95% 的混淆场景

以上就是 ProGuard 最核心的 5 条基础指令。我个人习惯在项目根目录放一个 proguard-rules.pro,按模块分块写规则,每块开头加注释说明用途。这样半年后再看,还能一眼知道当初为什么这么写。