4、编写Baseline Profile规则文件(baseline-prof.txt)

说到写规则文件,我记得第一次接触baseline-prof.txt时,看着那些密密麻麻的AST(抽象语法树)符号,心里直犯嘀咕——这不就是个纯文本吗?怎么还搞出这么多讲究。后来踩了几次坑才明白,规则文件写得好不好,直接决定Profile能不能生效、生效的覆盖面有多大。

说白了,这个文件就是用来告诉编译器:“这些代码路径是我App启动和交互中最关键的,你优先给我编译成机器码。” 它不像ProGuard规则那样复杂,但格式有严格约定。不然编译时就各种报错,比如格式错误、类名找不到、通配符滥用等问题。

规则文件长什么样

一个标准的baseline-prof.txt,每个规则占一行,支持以下几种模式:

类型 语法格式 示例 说明
类初始化 L<类路径>;
Lcom/example/MainActivity;
触发该类所有<clinit>和<init>方法
方法执行 <方法签名>
Lcom/example/MainActivity;->onCreate(Landroid/os/Bundle;)V
指定方法被编译,包含调用链中的热路径
通配方法 <签名> + ** 或 *
Lcom/example/utils/**;->*()V
匹配指定包下所有无参返回void的方法
构造函数 <init> 或 <clinit>
Lcom/example/MyHelper;-><init>()V
覆盖无参和有参构造函数
接口/注解标记 直接写入接口或注解类路径
Lkotlin/jvm/JvmStatic;
让编译器将含有该注解的类也一并编译
⚠️ 曾经踩过的坑: 规则必须以分号结尾,漏掉分号编译直接报“Missing semicolon”。另外签名中的箭头->前后不能有空格,我用编辑器自动排版时经常中招。

编写规则的核心流程

我个人习惯按照“分析热点 → 确定目标类 → 书写签名 → 验证覆盖”这四步走。下面这张图直观展示了规则编写的决策路径:

Baseline Profile 规则编写流程 1. 分析启动热点 使用 BaselineProfileGenerator 或 Macrobenchmark 2. 确定目标类/方法 优先选择启动路径、UI绘制链 3. 书写规则签名 注意分号、通配符、签名格式 类初始化规则 Lcom/…/MyClass; 方法执行规则 L…;->method(L…)V 4. 验证覆盖范围 通过 BaselineProfileRule 检查 💡 提示 建议先从启动路径的类规则入手 效果立竿见影

规则编写的注意事项

嗯,这里要重点说几个我在项目中反复翻车的点:

  • 规则数量限制: 单文件不要超过1500条规则,Android 12以上建议控制在1000条以内。我曾经一口气写了2000条,结果安装包体积膨胀了30%,启动速度反而变慢——因为AOT编译时间过长,系统直接放弃了部分规则。
  • 通配符的使用边界: ** 可以匹配任意多层级目录,* 只匹配一级。我建议优先用具体签名,只有确定整个包都需要覆盖时才用通配符。有个真实教训:Lcom/example/**;->*()V 会把所有无参void方法全扫进来,包括一些低频工具类,白白浪费编译空间。
  • 构造函数不要漏: 很多老师傅只写方法签名,忘了初始化逻辑。启动时对象的 <init> 如果不提前编译,第一个对象创建会触发解释执行。我一般会在规则里额外加一行:
    Lcom/example/splash/SplashViewModel;-><init>()V
  • 避免冗余覆盖: 如果已经写了类规则 Lcom/example/MyClass;,系统会自动编译该类所有成员方法。不需要再单独写里面的方法签名,写了反而可能导致基线重复合并时冲突。
我的小习惯: 每次写完规则文件后,我会跑一遍 ./gradlew :app:generateBaselineProfile,然后用 --info 日志查看 “Matched methods: X” 的数量。如果这个值远大于预期,就说明通配扩太广了,赶紧回去收窄。

一个完整规则的实战示例

假设我们有一个新闻类App,首页含网络请求、列表渲染、图片加载。我个人习惯这样组织规则:

# ===== 启动核心类 =====
Lcom/example/news/MainActivity;
Lcom/example/news/ui/NewsListFragment;

# ===== 网络库初始化 =====
Lcom/example/network/RetrofitClient;-><init>()V
Lcom/example/network/RetrofitClient;->createService(Ljava/lang/Class;)Ljava/lang/Object;

# ===== 图片加载路径 =====
Lcom/example/images/ImageLoader;->load(Ljava/lang/String;Landroid/widget/ImageView;)V
Lcom/example/images/ImageCache;->get(Ljava/lang/String;)Landroid/graphics/Bitmap;

# ===== 列表渲染高频方法 =====
Lcom/example/news/adapter/NewsAdapter;->onBindViewHolder(Lcom/example/news/adapter/NewsViewHolder;I)V
Lcom/example/news/adapter/NewsViewHolder;->bind(Lcom/example/news/model/NewsItem;)V

你看,我几乎没用一个通配符。每条规则都是我通过Macrobenchmark跑出来的热启动追踪路径。当然,如果你的应用有多个Module,别忘了在各自module的 src/main 下也放一份 baseline-prof.txt,否则子Module的代码不会被编译。

核心要义: 规则文件不是写越多越好,而是写准越好。精准定位到启动和交互中的热路径,AOT才能把力使在刀刃上。