Bazel 与 ProGuard 优化步骤完全指南:从 -optimizations 过滤器到 Gson 代码注入
2026/9/13 17:33:50 网站建设 项目流程

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/parametermethod/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/mathjava.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/advancedcode/removal/simple会强制开启它,你必须同时排除上游项,否则排除不生效。

JVM 级调优属性(非官方设置)

-optimizations外,ProGuard 还提供若干非官方的 JVM 系统属性来控制优化行为,它们通过-D...方式作为 JVM 参数传入,且可能在未来的版本中消失。这些属性对"内联膨胀"类问题非常实用:

系统属性默认值说明
maximum.inlined.code.length8字节可被内联的"短方法"最大代码长度(字节数)。内联过长的方法会无谓地膨胀代码体积
maximum.resulting.code.lengthJSE:8000字节;JME:2000字节内联后目标方法允许达到的最大代码长度。许多 JVM 不会对过长的方法应用即时编译(JIT),因此不能让方法长得过大
optimize.conservatively未设置允许输入代码中带有"故意抛异常但无其他用途"的普通指令(NullPointerExceptionArrayIndexOutOfBoundsExceptionClassCastException)。默认情况下 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 时直接访问领域类的字段,而不是走反射。该项优化带来的三个收益:

  1. 领域类可以放心混淆:与 Gson 一起使用的领域类不再受反射约束,类名与字段名可被自由重命名;
  2. 性能更好:注入的序列化代码直接访问字段,比依赖反射的 Gson 原始实现更快;
  3. 配置更少:优化会自动保留序列化所需的类与字段,开发者无需手工编写大量 keep 规则。

配置方式

Gson 优化默认启用,只要应用代码没有使用下文列出的不支持的 Gson 特性,就不需要任何额外配置(见 examples.md 的佐证)。

已知限制(无法优化的场景)

ProGuard无法优化以下 Gson 使用场景,一旦检测到,会自动为所有受影响的领域类保留原始的基于反射的 Gson 实现:

  • 序列化的类中使用了以下 Gson 注解之一:
    • @JsonAdapter
    • @Since
    • @Until
  • 序列化的类在签名中包含泛型类型变量(generic type variables);
  • 序列化使用的 Gson 实例由配置了以下选项的GsonBuilder构建:
    • excludeFieldsWithModifier
    • setFieldNamingPolicy

回退场景下的 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.MyMain

Android 场景: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.lengthmaximum.resulting.code.lengthoptimize.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询