Bazel 与 ProGuard 优化步骤完全指南:从 -optimizations 过滤器到 Gson 代码注入
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
本篇技术指南以 Bazel 仓库内置的 ProGuard 6.2.2 文档(third_party/java/proguard/proguard6.2.2/docs/manual/optimizations.md)为核心,系统讲解 ProGuard 优化步骤的开关控制、-optimizations过滤器的通配符语法、全部 34 类优化项的语义与隐含依赖关系、三个 JVM 级调优属性,以及默认开启的 Gson 序列化代码注入优化的原理与已知限制。读完本文,你将掌握在 Bazel Java 目标中按需裁剪 ProGuard 优化能力、规避激进优化风险、并为 Gson 序列化场景正确配置 keep 规则的方法。
优化步骤在 ProGuard 流程中的位置
ProGuard 是一个集**压缩(shrinking)、优化(optimization)、混淆(obfuscation)、预校验(preverification)**于一体的 Java 字节码处理器。在 Bazel 的 Java 构建链中,它以proguard.ProGuard为主类被包装为proguard可执行目标(见 tools/jdk/BUILD.java_tools),用于对打包产物执行上述处理。
优化步骤位于压缩与混淆之间:默认情况下 ProGuard 会对全部输入类文件执行优化——内联并合并类与类成员,并在字节码级别优化所有方法。该步骤由 usage.md 中定义的三个选项协同控制:
| 选项 | 作用 |
|---|---|
-dontoptimize | 完全不优化输入类文件 |
-optimizations | 在更细粒度上启用/禁用单个优化项(专家选项,仅优化开启时生效) |
-optimizationpasses n | 指定优化轮数,默认 1 轮;多轮可能带来进一步改进,若一轮后无改进则提前终止 |
与优化配套的还有-assumenosideeffects、-assumenoexternalsideeffects、-assumenoescapingparameters、-assumevalues等假设类选项,它们告诉 ProGuard 哪些方法没有副作用,从而允许删除未被使用的调用(典型场景是移除日志代码,见 examples.md)。这些选项同样仅适用于优化开启的情况,且官方文档明确警告"做出假设可能是危险的,容易破坏被处理代码,只有在你完全清楚后果时才使用"。
-optimizations与过滤器语法
-optimizations接受一个基于下方优化项名称的过滤器(filter)。过滤器的匹配语义与 ProGuard 其他所有过滤器(类名、文件名、属性名等)一致,具体规则定义在 usage.md 的 Filters 小节:
- 过滤器是逗号分隔的名称列表,仅名称与列表中某项匹配的优化会通过过滤;
- 通配符
?匹配优化名称中的任意单个字符; - 通配符
*匹配优化名称中的任意片段(在优化过滤器语境下*可以跨层级匹配,与通用过滤器中的**语义类似,可匹配包含斜杠分隔符在内的任意部分); - 名称前加感叹号
!表示排除,且被排除的名称不再参与与后续优化名称的匹配。
一个容易踩坑的细节是:ProGuard 不会检查过滤器的拼写错误。如果你写错了某个优化项名称,它会被静默当作一个不匹配任何东西的条目,优化照常进行,这可能导致你"以为关闭了某项优化,实际却并未关闭"。因此在配置-optimizations时必须格外小心,最好逐字对照官方名称列表。
官方文档给出了三个典型的过滤器示例:
-optimizations code/simplification/variable,code/simplification/arithmetic只执行指定的两个 peephole(窥孔)优化。
-optimizations !method/propagation/*执行所有优化,但排除在方法之间传播值的那几类(即method/propagation/parameter与method/propagation/returnvalue)。
-optimizations !code/simplification/advanced,code/simplification/*先排除code/simplification/advanced,再放行所有简化类优化,最终效果是只执行全部 peephole 简化优化,但不包括基于控制流/数据流分析的 advanced 简化。注意这里!只影响其后匹配到的名称——由于code/simplification/advanced先被显式排除,后续code/simplification/*不会再把它匹配进来。
优化项全目录:语义与隐含依赖
下面按类别完整列出 ProGuard 6.2.2 支持的全部优化项。每项的⇒标注表示该优化必然隐含启用的后续优化;*best used with*则表示推荐搭配(非强制)。注意:随着版本演进,优化项会被新增和重组,因此该列表在未来版本中可能变化。
库专项优化
| 优化项 | 说明 |
|---|---|
library/gson | 尽可能优化 Gson 库的使用方式,详见下文 Gson 优化专节 |
类(Class)级优化
| 优化项 | 说明 |
|---|---|
class/marking/final | 尽可能将类标记为final |
class/unboxing/enum | 尽可能将枚举类型简化为整型常量 |
class/merging/vertical | 尽可能在类层次结构中垂直合并类(父类与子类合并) |
class/merging/horizontal | 尽可能在类层次结构中水平合并类(兄弟类合并) |
class/merging/wrapper | 尽可能将包装类与其包装的类合并 |
字段(Field)级优化
| 优化项 | 说明 |
|---|---|
field/removal/writeonly | 删除只写不读的字段(⇒code/removal/advanced) |
field/marking/private | 尽可能将字段标记为private |
field/propagation/value | 跨方法传播字段的值(⇒code/simplification/advanced) |
方法(Method)级优化
| 优化项 | 说明 |
|---|---|
method/marking/private | 尽可能将方法标记为private(虚方法去虚化,devirtualization) |
method/marking/static | 尽可能将方法标记为static(虚方法去虚化,⇒code/removal/advanced) |
method/marking/final | 尽可能将方法标记为final |
method/marking/synchronized | 尽可能去除方法的synchronized标记(在可以证明线程安全的前提下) |
method/removal/parameter | 删除未被使用的方法参数(⇒code/removal/advanced) |
method/propagation/parameter | 将方法调用处的参数值传播到被调用方法内部(⇒code/simplification/advanced) |
method/propagation/returnvalue | 将方法的返回值传播到其调用处(⇒code/simplification/advanced) |
method/inlining/short | 内联短方法 |
method/inlining/unique | 内联只被调用一次的方法 |
method/inlining/tailrecursion | 尽可能简化尾递归调用 |
代码(Code)级优化
| 优化项 | 说明 |
|---|---|
code/merging | 通过修改分支目标合并相同的代码块 |
code/simplification/variable | 对变量加载/存储指令执行 peephole 优化 |
code/simplification/arithmetic | 对算术指令执行 peephole 优化 |
code/simplification/cast | 对类型转换指令执行 peephole 优化 |
code/simplification/field | 对字段加载/存储指令执行 peephole 优化 |
code/simplification/branch | 对分支指令执行 peephole 优化(⇒code/removal/simple) |
code/simplification/object | 对对象实例化执行 peephole 优化 |
code/simplification/string | 对常量字符串执行 peephole 优化 |
code/simplification/math | 对java.lang.Math方法调用执行 peephole 优化 |
code/simplification/advanced | 基于**控制流分析(CFA)与数据流分析(DFA)**简化代码(推荐与code/removal/advanced搭配使用) |
code/removal/advanced | 基于控制流分析(CFA)与数据流分析(DFA)删除死代码(⇒code/removal/exception) |
code/removal/simple | 基于简单的控制流分析删除死代码(⇒code/removal/exception) |
code/removal/variable | 从局部变量帧中删除未使用的变量 |
code/removal/exception | 删除 try 块为空的异常处理 |
code/allocation/variable | 优化局部变量帧上的变量分配(寄存器分配优化) |
从上面的清单可以看出 ProGuard 优化的整体策略:先做结构性的标记与合并(class/marking/*、class/merging/*),再通过去虚化与参数传播打开更多死代码删除的空间,最后用多轮 peephole 和基于 CFA/DFA 的 advanced 简化收尾。例如method/marking/static把虚方法转为静态方法后,调用点不再依赖多态分派,code/removal/advanced就能进一步删除因此变成不可达的分支。
隐含依赖链一览
部分优化之间存在强制的前置依赖,开启上游优化会自动启用下游优化:
field/removal/writeonly ⇒ code/removal/advanced field/propagation/value ⇒ code/simplification/advanced method/marking/static ⇒ code/removal/advanced method/removal/parameter ⇒ code/removal/advanced method/propagation/parameter ⇒ code/simplification/advanced method/propagation/returnvalue ⇒ code/simplification/advanced code/simplification/branch ⇒ code/removal/simple code/removal/advanced ⇒ code/removal/exception code/removal/simple ⇒ code/removal/exception理解这条依赖链对编写过滤器很重要:例如你若想仅排除code/removal/exception,由于code/removal/advanced或code/removal/simple会强制开启它,你必须同时排除上游项,否则排除不生效。
JVM 级调优属性(非官方设置)
除-optimizations外,ProGuard 还提供若干非官方的 JVM 系统属性来控制优化行为,它们通过-D...方式作为 JVM 参数传入,且可能在未来的版本中消失。这些属性对"内联膨胀"类问题非常实用:
| 系统属性 | 默认值 | 说明 |
|---|---|---|
maximum.inlined.code.length | 8字节 | 可被内联的"短方法"最大代码长度(字节数)。内联过长的方法会无谓地膨胀代码体积 |
maximum.resulting.code.length | JSE:8000字节;JME:2000字节 | 内联后目标方法允许达到的最大代码长度。许多 JVM 不会对过长的方法应用即时编译(JIT),因此不能让方法长得过大 |
optimize.conservatively | 未设置 | 允许输入代码中带有"故意抛异常但无其他用途"的普通指令(NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException)。默认情况下 ProGuard 会直接丢弃这类看似无用的指令,从而让大多数常见代码得到更好的优化;开启该属性则保守处理,保留这类指令 |
在 Bazel 中通过java_binary等目标的jvm_flags或执行脚本的JAVA_OPTS传入这些属性即可,例如设置-Dmaximum.inlined.code.length=16允许内联更长的短方法,或-Doptimize.conservatively开启保守优化模式以兼容"用异常做控制流"的旧代码。
Gson 优化:反射序列化的字节码级替换
library/gson优化是 ProGuard 6.x 引入的一项颇具代表性的能力,值得单独展开。
原理与收益
ProGuard 通过分析检测出哪些领域类(domain class)被 Gson 库用于序列化,然后用注入的、经过优化的直读/直写代码替换 Gson 基于反射的字段读写实现——即在读写 JSON 时直接访问领域类的字段,而不是走反射。该项优化带来的三个收益:
- 领域类可以放心混淆:与 Gson 一起使用的领域类不再受反射约束,类名与字段名可被自由重命名;
- 性能更好:注入的序列化代码直接访问字段,比依赖反射的 Gson 原始实现更快;
- 配置更少:优化会自动保留序列化所需的类与字段,开发者无需手工编写大量 keep 规则。
配置方式
Gson 优化默认启用,只要应用代码没有使用下文列出的不支持的 Gson 特性,就不需要任何额外配置(见 examples.md 的佐证)。
已知限制(无法优化的场景)
ProGuard无法优化以下 Gson 使用场景,一旦检测到,会自动为所有受影响的领域类保留原始的基于反射的 Gson 实现:
- 序列化的类中使用了以下 Gson 注解之一:
@JsonAdapter@Since@Until
- 序列化的类在签名中包含泛型类型变量(generic type variables);
- 序列化使用的 Gson 实例由配置了以下选项的
GsonBuilder构建:excludeFieldsWithModifiersetFieldNamingPolicy
回退场景下的 keep 配置
当上述任一特性被使用时,受影响领域类的序列化字段会重新依赖反射访问,因此在 ProGuard/DexGuard 配置中必须显式 keep这些字段,否则它们可能被压缩、优化或混淆掉,导致运行时反射找不到字段。原文档给出的通用做法(关闭优化时的等效配置)是在配置文件中加入:
-keepclassmembers class com.example.SerializedClass { <fields>; <init>(); }或借助@SerializedName注解批量保留字段(同时允许字段名混淆):
-keepclasseswithmembers,allowobfuscation,includedescriptorclasses class * { @com.google.gson.annotations.SerializedName <fields>; } -keepclassmembers enum * { @com.google.gson.annotations.SerializedName <fields>; }调试阶段还可以配合-addconfigurationdebugging选项,在运行时获得关于必要 keep 配置的反馈信息。
在 Bazel 构建中的实践要点
库级配置的合法性约束
Bazel 对库(library)级别的 ProGuard 配置有严格限制。仓库中的 tools/jdk/proguard_allowlister.py 明确说明:库的 ProGuard 配置只允许使用-keep、-assumenosideeffects、-assumevalues、以及带参数的-dontnote和-dontwarn。这样限制的原因正如该文件注释所述——防止某个库通过依赖传递,把"禁用混淆"这类影响面巨大的全局效果意外施加到整个二进制产物上。因此,像-optimizations这样的全局优化过滤选项,通常应在最终二进制目标(而非库目标)的配置中设置,并通过proguard_allowlister的校验。
实战配置示例
通用场景:在最终应用配置中执行最多 3 轮优化,让压缩、内联效果最大化(源自 examples.md):
-injars in.jar -outjars out.jar -libraryjars <java.home>/jmods/java.base.jmod(!**.jar;!module-info.class) -optimizationpasses 3 -overloadaggressively -repackageclasses '' -allowaccessmodification -keep public class com.example.MyMainAndroid 场景:Dalvik 1.0/1.5 无法处理某些算术简化结果,官方示例(见 examples.md)通过过滤器精确关闭该项:
-android -dontpreverify -repackageclasses '' -allowaccessmodification -optimizations !code/simplification/arithmetic -keep public class com.example.MyActivity这个例子是-optimizations过滤器最典型的用途:只排除已知在当前运行时环境下有问题的单项,其余优化全部保留。
Gson 项目:保持默认配置即可享受library/gson优化;若使用了不支持的 Gson 特性,则按上文"回退场景下的 keep 配置"补上 keep 规则。注意,若因兼容性问题整体关闭了优化(-dontoptimize),Gson 将退回反射实现,此时必须用-keepclassmembers保留序列化字段与无参构造器,否则运行时反射会失败。
小结
- 优化步骤由
-dontoptimize(总开关)、-optimizations(细粒度过滤器)与-optimizationpasses(轮数)共同控制,三者均定义于 usage.md 的 Optimization Options; - 优化过滤器沿用 ProGuard 通用过滤器语义:
?匹配单字符、*匹配任意片段、!排除且不回溯,且不校验拼写; - 34 类优化项可按 library / class / field / method / code 五个层级组织,多项优化之间存在
⇒隐含依赖,编写排除规则时要顺带排除上游项; - 三个 JVM 系统属性(
maximum.inlined.code.length、maximum.resulting.code.length、optimize.conservatively)用于控制内联膨胀与保守优化,属非官方设置,未来版本可能移除; library/gson默认启用,将 Gson 反射序列化替换为直接字段访问,带来可混淆、高性能、少配置三重收益;使用@JsonAdapter/@Since/@Until、泛型签名或受限GsonBuilder配置时自动回退反射实现,需手动补 keep 规则;- 在 Bazel 中,库级 ProGuard 配置受 proguard_allowlister.py 约束,全局优化选项应放在最终二进制目标配置中。
如需查阅更完整的配置选项、过滤器细节与更多实战示例,可继续阅读仓库内的 usage.md 与 examples.md。
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考