ProGuard 基础规则:混淆、优化、预校验

初学 ProGuard 时,我看到它那一大堆规则头都大了。后来才明白,它的核心其实就三件事:混淆、优化、预校验。说白了,就是把你的代码“改头换面”,再“减减肥”,最后“检查一下身体”,让它跑得更快、更难被逆向。

我个人习惯把这几个步骤串成一条流水线。今天我们就把它拆开,一个个看明白。

核心记忆点:ProGuard 的三大基础规则对应三个处理阶段——-obfuscate(混淆)、-optimize(优化)、-preverify(预校验)。默认这三项都是开启的,你可以在配置里用 -dontobfuscate、-dontoptimize、-dontpreverify 关掉它们。

ProGuard 处理流程(混淆、优化、预校验) 输入 JAR/APK 压缩(Shrink) 混淆(Obfuscate) 优化(Optimize) 预校验(Preverify) 输出优化后文件 预校验通常在最后一步 为 Android 4.0+ 脱机优化 默认开启,可手动关闭

第一步:混淆

混淆是大多数人最先接触的功能。它会重命名你的类、字段和方法,把 UserManager 变成 a,getUserName() 变成 b()。这么做的目的很直白——让反编译者看不懂你的代码。

嗯,这里要注意:混淆不是万能的。它只重命名,不改变逻辑。我在一个项目中遇到过,业务方把混淆后的堆栈直接扔给我,让我查崩溃原因。我一看全是一堆 a.a.b,根本没法定位。那以后我就养成了习惯——保存 mapping 文件。

我的小技巧:发布前把 mapping.txt 自动归档到 CI 构建产物中,命名带上版本号和构建时间。这样线上出问题,一秒就能反解堆栈。

混淆的常用配置有三类:

  • 保留规则:用 -keep 告诉 ProGuard 哪些类、方法不要动。
  • 不混淆特定类:比如 -keep class com.example.MyClass { *; }。
  • 适配第三方库:第三方 SDK 通常会在文档里给你一段 keep 规则,直接复制就行。

为什么需要 keep 规则?因为很多反射调用、JNI、序列化等场景,类名和方法名是硬编码的。一混淆,运行时找不到对应的类,直接 crash。我曾经吃过这个亏——一个网络请求框架用了反射,我没配 keep,结果接口全挂了,差点被测试打死。

第二步:优化

优化阶段比混淆更“激进”。它会内联短方法、移除无用分支、合并代码块,甚至把一些常量直接展开成值。说白了,就是编译器级别的代码微调,让你的程序跑起来更省指令、更省内存。

举个例子:

public int add(int a, int b) {
    return a + b;
}

// 调用处
int result = add(3, 5);

经过优化,可能直接变成:

int result = 8;

你想想看,如果你的代码里有一堆这样的小工具方法,优化后能省多少调用栈?尤其是 getter、setter,优化后直接消失。

但优化也有坑。我最怕的就是 循环优化 导致逻辑错误。以前我遇到过一个 Bug:代码里有一个 for 循环,循环体内有个空方法调用。ProGuard 优化时认为那个方法没副作用,直接把循环体干掉了,结果循环里的副作用逻辑(比如更新计数器)也跟着没了。从那以后,我对优化总是格外谨慎。

注意:优化默认是开启的,如果你怀疑优化导致运行时问题,可以先关闭:-dontoptimize。然后逐步开启个别优化项来定位。

第三步:预校验

预校验是很多人容易忽略的一步。它专门为 Android 4.0 (API 14) 及以上版本的虚拟机设计。目标是把字节码中需要运行时校验的部分提前完成,把校验结果直接写进 .class 文件里。这样 App 启动时,虚拟机就不需要再执行一遍校验了。

效果就是:启动速度更快。尤其在 Dalvik 虚拟机上,预校验能节省毫秒级的加载时间。虽然单个类不多,但成百上千个类累积起来,效果就很可观了。

ProGuard 默认就会做预校验,你不需要额外配置。但要注意一点:如果你是做 Java 标准库 或非 Android 项目的混淆,预校验可能会与目标环境冲突。我建议只在 Android 项目里保留它。

什么时候关掉预校验?

  • 当你的项目需要兼容 Android 2.x 以下版本时(几乎没有了)
  • 当你用 R8 替代 ProGuard 时(R8 内部已经处理了预校验)
  • 当你做混淆后单元测试发现 ClassLoader 报错,尝试关闭预校验

三者的配合

现在你应该明白了:混淆管“隐私”,优化管“性能”,预校验管“加载速度”。它们不是孤立的,而是一条流水线。

我记得有一次给一个大型项目做性能优化,只开了混淆,App 启动时间从 2.1 秒降到 1.8 秒。后来把优化也打开,降到 1.5 秒。再打开预校验,又降了 0.1 秒。虽然每步看起来不多,但加起来就是 30% 的提升。

当然,也要做好风险控制。我的习惯是:

  1. 先在 debug 包上只开混淆,跑通全量测试。
  2. 再在 release 包上全开,继续跑测试。
  3. 如果出现问题,先关优化,确认是不是优化引起的。
  4. 仍然不行,关预校验。
规则 作用 风险 建议
-obfuscate 重命名类、方法、字段 反射、JNI、序列化崩溃 必开,配好 keep 规则
-optimize 内联、剪枝、常量折叠 循环、反射相关逻辑异常 默认开,出问题时关
-preverify 提前完成字节码校验 兼容旧版虚拟机 Android 项目开,Java 项目可关

最后的建议:不要直接拿别人的配置模板就往项目里塞。每种规则都应该理解后再加入。我踩过的坑可以帮你省下很多调试时间。