ProGuard规则详解:-keep、-dontwarn、-keepclassmembers等核心指令
说实话,ProGuard的规则配置,是很多Android开发者的痛点。我见过太多项目,要么混淆规则写得过于宽松,导致APK体积没降下来;要么写得过于严格,一运行就崩溃。今天我就把最核心的几个指令掰开揉碎了讲清楚。
一、-keep:保命符,也是双刃剑
-keep 是ProGuard里最重要的指令,没有之一。它的作用就是告诉ProGuard:某些类、方法或字段,你千万别动。
为什么要保留?因为有些代码在运行时会被反射调用,或者被JNI调用,或者被XML布局文件引用。ProGuard是静态分析工具,它不知道这些动态调用,所以会误删。
核心语法:
-keep [修饰符] 类名 { 成员过滤器 }
我给大家拆解一下:
- 类名:可以是具体类名,也可以用通配符
- 成员过滤器:可选,指定要保留哪些成员(方法、字段)
- 修饰符:比如
public、protected、private等
举个例子:
-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 类名可能被混淆成 a、b 之类的。
注意:如果你既想保留类名,又想保留成员,请用 -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.MyClass 和 com.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.**
八、知识体系总览
为了让大家更直观地理解这些指令的关系,我画了一张图:
九、避坑指南
最后,我分享几个实战中踩过的坑:
我曾经犯过的错误:
- 过度使用
-keep:一开始为了省事,我把整个项目都-keep了,结果APK体积几乎没变。后来才明白,-keep要精准,不要图省事。 - 忘记保留
Parcelable的CREATOR:有一次序列化对象时崩溃,查了半天才发现是CREATOR被混淆了。从那以后,我每次都会加上-keepclassmembers class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator CREATOR; }。 - 忽略
-dontwarn的副作用:有个第三方库报了一堆警告,我直接加了-dontwarn **,结果线上崩溃了。后来发现那个库确实需要某些类,只是ProGuard分析时没找到。
嗯,ProGuard规则说难不难,说简单也不简单。关键是要理解每个指令的用途,然后根据项目需求精准配置。你想想看,如果连 -keep 和 -keepclassmembers 的区别都搞不清楚,那混淆配置迟早会出问题。
我个人建议,刚开始接触混淆时,先从 -keep 和 -dontwarn 入手,等熟悉了再慢慢加入其他指令。毕竟,安全加固是个循序渐进的过程,一口吃不成胖子。
公众号:蓝海资料掘金营,微信deep3321