每个 Android 开发者大概都经历过这个剧本:debug 包千好万好,release 包一打开就闪退,一查崩溃堆栈全是 a.b.c.a(Unknown Source)——恭喜,你被混淆背刺了。
这篇是我踩坑多年攒下的混淆配置笔记,覆盖参数设置、keep 规则语法、常用规则模板、规则叠加机制和资源压缩,最后聊聊 ProGuard 与 R8 该选谁。官方文档见 Shrink, obfuscate, and optimize your app。
一、混淆参数设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| -optimizationpasses 5 -verbose -dontoptimize -dontshrink -dontpreverify -dontobfuscate -ignorewarnings -dontwarn com.squareup.okhttp.** -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -printmapping mapping.txt -useuniqueclassmembernames -allowaccessmodification -renamesourcefileattribute SourceFile -keepattributes SourceFile,LineNumberTable -keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod -optimizations !code/simplification/cast,!field/ ,!class/merging/
|
二、keep 规则语法
保持规则的语法组成:
保持命令:
1 2 3 4 5 6
| -keep -keepnames -keepclassmembers -keepclassmembernames -keepclasseswithmembers -keepclasseswithmembernames
|
类的匹配:
- 访问修饰符:
public、private、protected
- 通配符
*:匹配任意长度字符,但不包含包名分隔符 .
- 通配符
**:匹配任意长度字符,且包含包名分隔符 .
extends:匹配某个父类的子类
implements:匹配实现了某接口的类
$:内部类
成员的匹配:
<init>:匹配所有构造器
<fields>:匹配所有域
<methods>:匹配所有方法
- 除了
* 和 **,还支持 *** 通配符,匹配任意参数类型
...:匹配任意长度的任意类型参数,如 void test(...) 可以匹配不同参数个数的 test 方法
三、常用混淆规则模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| -keep class cn.listenergao.myapp.example.Test { *; }
-keep class cn.listenergao.myapp*
# 不混淆指定包名下的类名,及类里的内容 -keep class cn.listenergao.myapp* {*;}
-keep class cn.listenergao.myapp**
# 不混淆某个类的子类 -keep public class * extends cn.listenergao.myapp.base.BaseFragment
# 不混淆实现了某个接口的类 -keep class * implements cn.listenergao.myapp.dao.DaoImp
# 不混淆类名中包含了"entity"的类,及类中内容 -keep class **.*entity*.** {*;}
-keep class cn.listenergao.myapp.widget.CustomView$OnClickInterface { public *; }
-keep class cn.listenergao.myapp.example.Test{ <init>; <fields>; <methods>; }
-keep class cn.listenergao.myapp.example.Test{ public <methods>; private <methods>; public <init>(java.lang.String); }
-keep class cn.listenergao.myapp.example.Test$MyClass{*;}
-keepclasseswithmembernames class * { native <methods>; }
-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }
-keepclassmembers class **.R$* { public static <fields>; }
-keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); }
-keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; }
|
注意事项(每一条背后都是一次线上事故,建议裱起来):
- JNI 方法不可混淆,方法名需与 native 方法保持一致
- 反射用到的类不混淆,否则反射可能出问题
- 四大组件、Application 子类、Framework 层下的类、自定义 View 默认不会被混淆,无需另外配置
- WebView 的 JS 调用接口方法不可混淆
- 注解相关的类不混淆
- GSON、Fastjson 等解析的 Bean 数据类不可混淆
- 枚举类中的 values 和 valueOf 这两个方法不可混淆(反射调用)
- 继承 Parcelable 和 Serializable 等可序列化的类不可混淆
- 第三方库或 SDK 参考第三方提供的混淆规则,没提供的话建议第三方包全部不混淆
四、混淆规则的叠加
你有没有想过一个灵异现象:主模块的 proguard-rules.pro 明明空空如也,代码却被混淆了?这不是闹鬼,是因为混淆规则是叠加的,来源远不止你写的那一份:
- 各模块的 proguard-rules.pro —— 子模块也可以有,某个模块的配置可能影响其它模块
- proguard-android-optimize.txt —— AGP 编译时生成,包含对大多数 Android 项目有用的规则,并启用
@Keep* 注解;AGP 还提供 proguard-defaults.txt 和 proguard-android.txt,可通过 getDefaultProguardFile 设置,建议用 optimize 这份(多了优化配置)
- build/intermediates/proguard-rules/debug/aapt_rules.txt —— AAPT2 根据清单文件、布局及其他资源的引用自动生成保留规则,比如不混淆每个 Activity
- AAR 库的
/proguard.txt
- Jar 库的
/META-INF/proguard/
想查看所有规则叠加后的最终混淆规则,在主模块 proguard-rules.pro 添加:
1
| -printconfiguration ./build/outputs/mapping/full-config.txt
|
五、资源压缩
资源压缩分两步:资源合并与资源移除。
资源合并:无论是否配置 shrinkResources true,AGP 构建 APK 时都会执行。存在两个或更多同名资源时,AGP 从重复项中选择优先级更高的文件传递给 AAPT2。级联优先顺序:
比如重复资源存在于主资源及渠道中,Gradle 会选渠道中的资源;但如果重复资源在同一层次(如 src/main/res/ 和 src/main/res2/),Gradle 会报资源合并错误。
资源移除:开启资源压缩后,所有未被使用的资源默认会被移除。想自定义保留/移除,可在 res/raw/ 下创建 keep.xml(此文件不会打包进 APK,支持通配符 *):
1 2 3 4 5
| <?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*" tools:discard="@layout/unused2" tools:shrinkMode="strict"/>
|
shrinkMode 可选 strict / safe:前者严格按 keep 和 discard 指定的资源保留;后者保守删除未引用资源(如代码中用 Resources.getIdentifier() 引用的资源会保留)。
另外还可以用 resConfigs 移除不需要的备用资源,比如只保留中文:
1 2 3 4 5
| android { defaultConfig { resConfigs "zh-rCN" } }
|
六、ProGuard 与 R8
- ProGuard:压缩、优化和混淆 Java 字节码的免费工具,开源仓库
- R8:ProGuard 的替代工具,兼容现有 ProGuard 规则,更快更强。AGP 3.4.0 及以上默认使用 R8
结论:直接用 R8,别纠结。兼容绝大部分 ProGuard 规则、编译更快、对 Kotlin 更友好——想换回 ProGuard 属于「可以但没必要」系列。
构建后会在 build/outputs/mapping/release/ 输出以下文件:
- mapping.txt:原始与混淆后的类、方法、字段名称映射。崩溃堆栈还原全靠它,发版后务必留存——弄丢 mapping 文件再看线上崩溃,就像拿着一张全是马赛克的藏宝图
- seeds.txt:未进行混淆的类与成员
- usage.txt:APK 中被移除的代码
- resources.txt:资源优化记录
这些文件不一定都有,可以在 proguard-rules.pro 显式指定输出:
1 2 3
| -printmapping ./build/outputs/mapping/mapping.txt -printseeds ./build/outputs/mapping/seeds.txt -printusage ./build/outputs/mapping/usage.txt
|