Keep 规则详解:-keep、-keepclassmembers、-keepclasseswithmembers
这三个规则,说白了就是 ProGuard 混淆的“看门人”。你用好了,该留的留,该删的删。用不好,要么运行时崩给你看,要么包体积蹭蹭往上涨。我见过太多人把 -keep 当万能药,结果混淆等于没开。
今天我们就掰开揉碎,把这三个规则的语法、场景、坑点讲清楚。你想想看,搞懂了它们,你就能精准控制混淆的“火力范围”。
1. 三兄弟的核心区别
先看一张表,直观感受一下差异:
| 规则 | 保留范围 | 典型场景 |
|---|---|---|
-keep |
类本身 + 所有匹配的成员 | 反射入口、序列化、JNI 接口 |
-keepclassmembers |
仅匹配的成员,不保留类 | 回调方法、注解字段、枚举 values() |
-keepclasseswithmembers |
当成员匹配时,保留类 + 成员 | 保留特定结构的接口或基类 |
为什么会有三个?其实很好理解。-keep 是最刚的——只要类在,成员就保。-keepclassmembers 只关心成员,类本身可以走。-keepclasseswithmembers 则是个“条件触发”规则:成员若在,类才保。这三个层层递进,应对不同场景。
-keep = 保类保成员,一保到底。
-keepclassmembers = 只保成员,类可丢。
-keepclasseswithmembers = 成员若在,类才保。
2. -keep:保类保成员,一保到底
语法很简单:
-keep [修饰符] 类名 {
成员匹配器;
}
只要类名匹配上,类本身和所有匹配的成员都会被保留。成员匹配器可以用 * 匹配所有方法/字段,也可以用 <init> 匹配构造方法。
举个例子:
-keep class com.example.api.ApiService {
public <methods>;
}
这条规则保住了 ApiService 类以及它所有的 public 方法。我在项目中遇到过这样一件事:一个反射调用死活拿不到方法,查了半天——类被保留了,但方法被混淆了。就是因为只写了 -keep class 没写成员匹配器。
* 通配所有成员。但别滥用,否则混淆效果大打折扣。
典型场景:
- 通过 Class.forName() 反射加载的类
- 实现了 Serializable 的类(防止字段名变化破坏反序列化)
- JNI 中 native 方法对应的 Java 类
3. -keepclassmembers:只保成员,不保类
语法一模一样,但语义完全不同:
-keepclassmembers class com.example.model.User {
public <methods>;
private *;
}
这条规则只保留 User 类中的 public 方法和所有私有成员,但类名本身如果没被其他地方引用,照样会被混淆。说白了,就是“保人不保户”。
我记得有一次混淆后,某个按钮点击事件没反应了。排查发现是回调接口的方法被重命名了。接口方法被混淆,导致框架通过方法名匹配失败。加了一条 -keepclassmembers 保留所有回调方法,问题就解决了。
典型场景:
- 保留枚举类的 values() 和 valueOf() 方法
- 保留注解处理器要扫描的注解字段
- 保留某些框架约定的回调方法(如 EventBus 的 onEvent)
4. -keepclasseswithmembers:条件触发,保类保成员
这个规则稍微绕一点。它的逻辑是:如果类中存在匹配的成员,那么类本身 + 这些成员一起保留。 如果类中没有匹配的成员,那啥也不保留。
-keepclasseswithmembers class com.example.core.BasePresenter {
<init>(android.content.Context);
}
这条规则的意思是:如果 BasePresenter 类有一个接收 Context 的构造方法,那就把整个类保留下来。如果没有这样的构造方法,那就不管。
为什么要这么设计?你想想看,有时候我们想保留“满足某种结构特征”的类。比如所有继承了某个基类且实现了特定接口的类,都可以用这个规则一键保留。
我曾经处理过一个第三方 SDK 的混淆配置。SDK 文档要求保留所有继承自 SdkBase 且含有 init(Context) 方法的类。用 -keepclasseswithmembers 一条规则就搞定了,比一个个列 -keep 优雅得多。
典型场景:
- 保留所有含有特定构造方法的 Activity
- 保留所有实现了某个接口并包含特定方法的类
- 动态代理中用到的目标接口
5. 避坑指南与个人经验
下面这几个坑,我基本都踩过。你遇到了可以直接翻到这里对照。
- 坑一:-keep 过多导致包体积暴增。 我曾经接手一个项目,几乎每个类都加了 -keep,结果混淆后包体积只小了 5%。后来逐步替换成 -keepclassmembers,体积多缩减了 20%。
- 坑二:-keepclasseswithmembers 的成员匹配器写得太宽。 一个通配符
*可能会匹配到你不想要的成员,导致预期外的类被保留。匹配器越精确越好。 - 坑三:混淆后出现 ClassNotFoundException。 优先检查是不是漏了 -keep。用
whyareyoukeeping命令可以反向追踪某个类为什么被保留或为什么被移除。
-keep。
最后聊一下选型建议。我个人习惯按这个优先级来:
- 能写
-keepclassmembers就绝不写-keep。 - 如果类必须保留,先试试
-keepclasseswithmembers能不能满足。 - 只有确认“类名 + 成员”都必须原样保留时,才用
-keep。
这三个规则并不互斥,它们可以组合使用。比如用 -keep 保留核心 API 类,用 -keepclassmembers 保留内部的回调方法,用 -keepclasseswithmembers 批量保留具有特定结构的扩展类。
记住一句话:混淆配置不是越少越好,而是越精准越好。你把这三个规则吃透了,ProGuard 就不再是玄学,而是你手里的手术刀。