ProGuard规则详解:-keep、-dontwarn、-keepclassmembers等核心指令

说实话,ProGuard的规则配置,是很多Android开发者的痛点。我见过太多项目,要么混淆规则写得过于宽松,导致APK体积没降下来;要么写得过于严格,一运行就崩溃。今天我就把最核心的几个指令掰开揉碎了讲清楚。

一、-keep:保命符,也是双刃剑

-keep 是ProGuard里最重要的指令,没有之一。它的作用就是告诉ProGuard:某些类、方法或字段,你千万别动。

为什么要保留?因为有些代码在运行时会被反射调用,或者被JNI调用,或者被XML布局文件引用。ProGuard是静态分析工具,它不知道这些动态调用,所以会误删。

核心语法:

-keep [修饰符] 类名 { 成员过滤器 }

我给大家拆解一下:

  • 类名:可以是具体类名,也可以用通配符
  • 成员过滤器:可选,指定要保留哪些成员(方法、字段)
  • 修饰符:比如 publicprotectedprivate

举个例子:

-keep public class com.example.MyClass {
    public void myMethod();
    public int myField;
}

这条规则的意思是:保留 MyClass 类,以及它的 myMethod 方法和 myField 字段,不混淆也不删除。

我的经验:我个人习惯把 -keep 规则按模块分组,比如网络层、数据库层、UI层各写一块。这样后期维护时,一眼就能找到问题所在。

二、-keepclassmembers:只保留成员,不保留类

这个指令和 -keep 很像,但有一个关键区别:-keepclassmembers 只保留类的成员(方法和字段),不保证类本身不被混淆或删除。

什么时候用?比如你有一个类,类名无所谓,但里面的某个方法必须保留原名,因为会被反射调用。

-keepclassmembers class com.example.MyClass {
    public void callBack();
}

这条规则只保留 callBack 方法,MyClass 类名可能被混淆成 ab 之类的。

注意:如果你既想保留类名,又想保留成员,请用 -keep,而不是 -keepclassmembers。我曾经在项目中犯过这个错误,结果反射调用时类名变了,找了半天bug。

三、-keepnames:只保留名字,不阻止删除

-keepnames-keep 的变体,它只保留名字(不混淆),但不阻止ProGuard删除未使用的类或成员。

说白了就是:如果这个类没被用到,它还是会被删掉;但如果被用到了,它的名字不会被混淆。

-keepnames class com.example.MyClass

这条规则的意思是:如果 MyClass 被用到了,保留它的原名;如果没被用到,删掉也无所谓。

四、-dontwarn:闭嘴指令

-dontwarn 可能是被用得最多的指令了。它的作用是让ProGuard闭嘴,不要输出某些警告信息。

为什么需要它?因为有些第三方库的代码里,引用了不存在的类或方法,ProGuard在分析时会报警告。这些警告通常不影响运行,但看着烦人,而且会打断CI构建。

-dontwarn com.example.thirdparty.**
-dontwarn javax.annotation.**

重要提醒:-dontwarn 是治标不治本。如果你不确定警告是否安全,最好先查清楚。我曾经因为盲目使用 -dontwarn,导致线上崩溃,原因是某个第三方库确实需要那些类。

五、-keepattributes:保留元数据

这个指令用来保留类、方法、字段上的注解、签名、内部类等信息。很多框架(比如Gson、Retrofit)依赖这些元数据。

-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
-keepattributes EnclosingMethod

我建议你至少保留 *Annotation*Signature,否则很多框架会出问题。

六、通配符与匹配规则

ProGuard支持多种通配符,掌握它们能让你写出更简洁的规则:

通配符 含义 示例
? 匹配单个字符 com.example.? 匹配 com.example.a,不匹配 com.example.ab
* 匹配任意字符,但不包含包分隔符 com.example.* 匹配 com.example.MyClass,不匹配 com.example.sub.MyClass
** 匹配任意字符,包含包分隔符 com.example.** 匹配 com.example.MyClasscom.example.sub.MyClass
*** 匹配任意类型(用于方法参数) void method(***) 匹配任意参数类型的方法

我的习惯:对于整个包下的所有类,我通常用 **,比如 -keep class com.example.** { *; }。但要注意,这样会保留所有内部类,有时候会导致APK体积变大。

七、实战:一个完整的规则示例

下面是我在一个实际项目中用到的规则片段,大家可以参考:

# 保留Application类
-keep class com.example.MyApplication { *; }

# 保留所有Activity,不混淆类名和方法
-keep class com.example.ui.** { *; }

# 保留Gson用到的数据类
-keep class com.example.model.** { *; }

# 保留JNI方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留枚举
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留Parcelable实现
-keepclassmembers class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator CREATOR;
}

# 保留R文件
-keep class **.R$* { *; }

# 忽略某些警告
-dontwarn okhttp3.**
-dontwarn retrofit2.**

八、知识体系总览

为了让大家更直观地理解这些指令的关系,我画了一张图:

ProGuard核心指令知识体系 ProGuard规则 保留类 -keep class ... -keepnames class ... 保留成员 -keepclassmembers ... -keepclassmembernames ... 控制警告 -dontwarn ... -ignorewarnings 保留属性 -keepattributes ...

九、避坑指南

最后,我分享几个实战中踩过的坑:

我曾经犯过的错误:

  • 过度使用 -keep:一开始为了省事,我把整个项目都 -keep 了,结果APK体积几乎没变。后来才明白,-keep 要精准,不要图省事。
  • 忘记保留 ParcelableCREATOR:有一次序列化对象时崩溃,查了半天才发现是 CREATOR 被混淆了。从那以后,我每次都会加上 -keepclassmembers class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator CREATOR; }
  • 忽略 -dontwarn 的副作用:有个第三方库报了一堆警告,我直接加了 -dontwarn **,结果线上崩溃了。后来发现那个库确实需要某些类,只是ProGuard分析时没找到。

嗯,ProGuard规则说难不难,说简单也不简单。关键是要理解每个指令的用途,然后根据项目需求精准配置。你想想看,如果连 -keep-keepclassmembers 的区别都搞不清楚,那混淆配置迟早会出问题。

我个人建议,刚开始接触混淆时,先从 -keep-dontwarn 入手,等熟悉了再慢慢加入其他指令。毕竟,安全加固是个循序渐进的过程,一口吃不成胖子。


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