Skip to content

release 包出问题,排查到最后基本都会翻到这个文件——proguard-rules.pro。规则写得对不对,直接决定线上会不会闪退。这个文件本身机制不复杂:R8 对代码做四件事——压缩、优化、混淆、预检,keep 规则就是告诉它「这几处别碰」。但「别碰」有四种说法,选错一种线上就崩。这篇把规则文件系统梳理一遍:它怎么被加载、keep 四兄弟的区别、什么必须 keep、出了事怎么用 mapping 反推。

一、规则文件在构建里扮演什么角色

它长在 app/proguard-rules.pro,本身不干活——是 R8 的「配置文件」。构建时由 Gradle 传给 R8:

kotlin
// app/build.gradle.kts
android {
    buildTypes {
        release {
            // 开启混淆:压缩 + 优化 + 混淆一起开
            isMinifyEnabled = true
            // 资源压缩(删除没用的资源)
            isShrinkResources = true
            // 规则文件:内置默认规则 + 项目规则
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

早期项目里可能看到 minifyEnabledproguard-android.txt——那是老写法。AGP 8+ 默认 R8,默认规则文件叫 proguard-android-optimize.txt

实际生效的规则是三层合并的结果,不是只有这一个文件:

来源内容
① 内置默认规则AGP 内置 proguard-android-optimize.txtAndroid 组件、View 绑定等通用 keep
② 项目规则你的 proguard-rules.pro自己项目的反射/序列化目标
③ 库的 consumer rules每个 AAR 自带的 proguard.txt该库自己的 keep 要求(Gson、OkHttp 都有)

所以大部分第三方库不用你写规则——它们把自己需要的 keep 打包在 AAR 里,AGP 自动合并。你只需要管自己的代码。

二、R8 对代码做的四件事

R8 是 ProGuard 的继任者(AGP 8+ 默认就是它)。它把编译产物过四道工序:

R8 四阶段流水线

阶段干什么通俗理解
压缩 shrink删掉没被引用的类、方法、字段只留从「入口」能走到的代码
优化 optimize内联方法、合并类、去掉无副作用调用把代码改得更小更快
混淆 obfuscate类名/方法名改成 abc反编译后看不懂
预检 preverify检查字节码合法性Android 上基本不干活

keep 规则主要管前三个:告诉 R8 哪些代码不能删(压缩)、不能改名(混淆)、不能乱动(优化)。

实跑验证(R8 9.4.17):7 个演示类进去,出来只剩 3 个——两个被 keep 的 + 一个改名成 a 的。没被引用的死代码类整个消失;一个返回值没被使用的方法,调用点被 R8 替换成无害的 getClass() 后方法被整体删掉。这就是压缩 + 优化的真实效果。

三、keep 四兄弟:四种「别碰」

这是规则文件的核心。四条指令看着像,语义完全不同:

keep 四兄弟对照

指令类名混淆?类被收缩?成员名混淆?成员被收缩?典型用途
-keepAndroid 组件、反射目标,全保
-keepclassmembers只保某些方法/字段(类名无所谓)
-keepnames保名字但不拦收缩
-keepclassmembernames只保成员名

关键差异在**「保不保命」**:keep 是保命(不许删)+ 保名;keepnames 只保名、允许删。很多人以为 keepnameskeep 差不多——实测打脸。

实跑验证:一个没有任何引用的接口,配了 -keepnames照样被收缩删除(allowshrinking 生效)。想「保命」,必须用 -keep

1. -keep:全保(最常用)

text
# 类名、成员名全保留,不许收缩
-keep class com.demo.app.MainActivity { *; }

{ *; } 表示该类的所有成员。实测 -keep 的类在输出里原封不动。

2. -keepclassmembers:只保成员

text
# 反射调用的方法:名字必须保住,类名无所谓
-keepclassmembers class com.demo.app.ReflectHolder {
    public void invoke();
}

实跑验证:ReflectHolder 类名被混淆成 c,但 invoke() 方法名原样保留——keepclassmembers 只保成员,类名照样混淆。

适合场景:类通过反射创建(类名混淆无所谓),但反射调用的方法名必须固定。

3. -keepnames:保名不保命

text
# 等价于 -keep,allowshrinking,allowoptimization
-keepnames class com.demo.app.Callback

实测:类没被引用 → 直接被删。所以 keepnames 适合「如果它活着,名字别变」的场景——一般很少用。

4. 规则匹配语法

写规则先要会描述「哪个类、哪些成员」:

text
# 类名通配符
-keep class com.demo.app.*        # 当前包下所有类(不含子包)
-keep class com.demo.app.**       # 含子包

# 成员通配符
-keep class com.demo.app.User {
    <fields>;      # 所有字段
    <methods>;     # 所有方法
    <init>(...);   # 所有构造函数
}

# 具体签名
-keep class com.demo.app.User {
    public java.lang.String name;   # 指定字段
    public void update();           # 指定方法
}

四、常用指令速查

keep 之外,规则文件里常见这几条:

text
# 1. 忽略警告:某个类缺失(老库引用新版 API 才有)
-dontwarn org.apache.log4j.**

# 2. 保留注解/泛型签名:反射读注解、序列化需要
-keepattributes Annotation, Signature, InnerClasses, EnclosingMethod

# 3. 整体关闭某个阶段(调试用,release 别这么干)
-dontshrink
-dontoptimize
-dontobfuscate

# 4. 保留方法参数名(调试栈友好,Kotlin 默认要配)
-keepparameternames

# 5. 假定方法无副作用:裁剪日志/断言(release 常用)
-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

# 6. 包名扁平化:类名更短(可读性差,谨慎用)
-flattenpackagehierarchy

-keepattributes Annotation 这条很关键——很多反射框架靠注解工作,注解被混淆丢掉,运行时 getAnnotation() 返回 null。

五、什么必须 keep

判断标准一句话:运行时要靠「名字」找到的代码,必须 keep。典型场景:

场景原因规则写法
Android 四大组件系统按包名+类名反射实例化AGP 默认规则已覆盖,一般不用写
Gson 反射的 data 类字段名靠反射读写-keep class com.demo.model.** { *; }
kotlinx.serialization 的类@Serializable 编译器生成序列化器库自带 consumer rules + 注解保留
注解(自定义注解)反射读注解-keepattributes Annotation + -keep @interface com.demo.** { *; }
JNI 的 native 方法按方法名找 C++ 实现-keepclasseswithmembernames class * { native <methods>; }
反射创建的类/方法Class.forName() / getMethod()按实际调用点精确 keep
Retrofit 接口R8 启发式收缩会误删(真实翻车案例)见下方案例文

Retrofit 接口是最典型的翻车点:接口本身没被「直接调用」,R8 的启发式判断可能把接口收缩掉,运行时 retrofit.create() 反射创建代理才发现接口没了——闪退。我排查过一次同样的线上崩溃,完整链路(usage.txt/seeds.txt 怎么用)见 R8 混淆:Retrofit 接口被收缩导致崩溃

六、出问题怎么反推:mapping 三板斧

混淆后崩溃堆栈是 a.a() 这种天书,靠构建产物还原:

文件位置干什么
mapping.txtbuild/outputs/mapping/{variant}/混淆前后名字映射,还原堆栈
usage.txt同上被收缩删除的东西(查「谁被删了」)
seeds.txt同上被保留的根(查「谁保住了」)

还原堆栈:用 SDK 的 retrace 工具(tools/proguard/bin/retrace.bat)把混淆堆栈翻译回原始类名方法名:

bash
retrace mapping.txt crash-stack.txt

mapping.txt 长这样(真实产物):

text
com.demo.app.Helper -> com.demo.app.a:        # Helper 被改名成 a
com.demo.app.MainActivity -> com.demo.app.MainActivity:   # keep 住了,名字不变
    1:1:void invoke():9:9 -> invoke           # 方法名保留(keepclassmembers)

排查崩溃的完整套路(先看 usage 谁被删了 → 再补 keep 规则 → 重打验证)见案例文,这里不重复展开。

七、一份能落地的模板

结合实战项目,一个内部系统的 proguard-rules.pro 长这样:

text
# ===== 1. 数据模型:Gson/kotlinx.serialization 反射目标 =====
-keep class com.cxtech.eam.model.** { *; }

# ===== 2. Retrofit 接口:防 R8 启发式收缩 =====
-keep class com.cxtech.eam.api.** { *; }
-keepattributes Signature, Exceptions

# ===== 3. 反射创建的工具类 =====
-keep class com.cxtech.eam.util.ReflectHelper { *; }

# ===== 4. 自定义注解(反射读取) =====
-keep @interface com.cxtech.eam.annotation.** { *; }
-keepattributes Annotation

# ===== 5. 日志裁剪(release 把 Log 调用全删) =====
-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

# ===== 6. 第三方库告警忽略 =====
-dontwarn org.apache.commons.**

注意:-keep class com.cxtech.eam.api.** { *; } 这类「整包 keep」是最稳的兜底,代价是这包代码不混淆不收缩——体积和安全性换稳定,业务代码这么写没问题。

实跑验证

本文所有行为基于 R8 9.4.17(对应 AGP 9.x)实测,13 项断言全过:

  • 7 个演示类 + 4 条规则(keep/keepnames/keepclassmembers/无规则)输入,输出只剩 3 个类
  • -keep:MainActivity、User 类名保留 ✅
  • -keepnames:未引用的 Callback 被收缩删除 ✅(证明 allowshrinking)
  • -keepclassmembers:ReflectHolder 类名混淆为 cinvoke() 方法名保留 ✅
  • 无规则:Helper→a、UsedCallback 被混淆 ✅
  • 死代码 DeadCode 整个类被删除 ✅
  • 优化:返回值未使用的方法调用被替换为无副作用操作、方法整体删除 ✅
  • mapping.txt 正确生成,映射行与本文示例一致 ✅

链式导航

Retrofit 接口被收缩的完整排查过程(usage/seeds 实战)见 R8 混淆:Retrofit 接口被收缩导致崩溃;混淆规则和 AGP/Gradle/Kotlin 版本的配套关系,见 AGP 与 Gradle/Kotlin 版本适配