手动编写 baseline-prof 文件:格式、注释与基本规则

说实话,刚开始接触 Baseline Profile 的时候,我总觉得用 Android Studio 自带的插件生成就完事了。直到有一次线上应用启动慢了一截,我打开生成的 prof 文件,发现里面居然有几十个根本不会被调到的冷门类——嗯,从那以后,我就开始老老实实手写 prof 了。你想想看,编译器不会替你思考哪些路径是用户真正关心的,只有你最清楚自己 App 的热点。

手动写 baseline-prof,说白了就是在 /app/src/main/ 下放一个纯文本文件,取名叫 baseline-prof(不带任何扩展名)。它的格式极其简单,但规则细节特别容易踩坑。我来拆开讲。

文件格式:一个纯文本的「规则清单」

每一行就是一条规则。规则由操作码 + 目标描述组成。空行会被忽略,注释以 # 开头。文件第一行必须是包名声明,就像给它一个身份证。

# 文件第一行:声明包名
com.example.myapp

# 下面都是规则行
HSPLcom/example/myapp/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/example/myapp/data/Repository;->fetchData()Ljava/util/List;
WSLcom/example/myapp/ui/HomeFragment;

注意那个 #HSPL ?不对,我写错了——操作码和描述之间没有空格,但注释符号 # 是独立于规则行的。上面示例里第二行其实应该是 HSPL 开头,但我故意用了个错误写法来提醒你:每一行规则前面千万不要加 # 号,否则整行都会被当成注释。

我曾经踩过的坑:有一次我把规则行全部复制粘贴,不小心在每行前面多了一个空格。结果编译没报错,但生成的 profile 体积直接翻倍——因为每条规则都被当作无效行忽略了。ALint 工具在 4.2 之后会检查,但早期版本根本不提示。所以写完以后一定用 adb shell profman --dump 看一遍实际生效的规则。

规则结构:五个操作码,三种核心类型

每个规则由三个字母的操作码开头,后面紧跟类的描述符。操作码决定了这条规则在编译和运行时的作用。我把常用的几类整理成了一个表格,方便你对照。

操作码 含义 示例
HSPL 启动时预加载(Hot Start Path Load) HSPLcom/example/myapp/MainActivity;->onCreate(Landroid/os/Bundle;)V
WSL 类全量预加载(Warm Start Load) WSLcom/example/myapp/data/DataSource;
WBL 引用预热(Warm Boot Load) WBLandroidx/recyclerview/widget/RecyclerView
SPL 路径预编译(Startup Path Load) SPLcom/example/myapp/di/AppModule;
R 纯引用(Reference) Randroid/content/Context;

你可能会问:「到底用哪个?」我的经验是:HSPL 只放用户冷启动时真正调用的方法和类;WSL 适合那些虽然不立刻用到,但后续大概率会触发的类(比如 ViewPager 的 Fragment);WBL 和 SPL 主要用于加速系统组件的编译,一般不用手写,除非你明确知道某个库的初始化链很长。R 码则是最轻量的,只加入依赖引用,不会触发生成。

我个人习惯:先跑一次自动化工具生成一个初始 prof,然后人工审查。把那些明显不会在首屏出现的规则删掉,再手动加上 HSPL 和 WSL 混合方案。核心原则是——宁少勿多,每条规则都应该是经过验证的热点。
baseline-prof 文件结构 prof 文件 # 第一行:包名声明 (com.example.myapp) # 下面都是规则行,空行忽略 规则类型 HSPL 启动时预加载 (方法 + 类) 描述符 WSL 类全量预加载 (整个类) WBL 引用预热 (系统组件) SPL 路径预编译 (初始化链) R 纯引用 (不触发编译) 包含 核心规则: • 每条规则必须以操作码开头,后跟类/方法描述符,无空格 • 方法描述符需包含括号和返回值(如 (Landroid/os/Bundle;)V) • 类描述符使用 / 分隔包名,分号结尾(如 Lcom/example/MyClass;)

注释:你的「设计文档」

纯文本文件虽然简单,但一不留神就会变成一堆天书。我强烈建议在 prof 文件里加满注释。注释以 # 开头,可以单独占一行,也可以写在规则行后面(不过我个人觉得写在前面更清晰)。

# ---- 首屏启动链 ----
HSPLcom/example/myapp/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/example/myapp/di/AppComponent;->initialize()V

# ---- 懒加载但高频使用 ----
WSLcom/example/myapp/data/UserRepository;
WSLcom/example/myapp/network/ApiService;

# ---- 第三方库预热 ----
WBLandroidx/recyclerview/widget/RecyclerView
WBLcom/google/android/material/tabs/TabLayout

你看,加注释后,别人(甚至一个月后的你自己)拿到文件就能一眼看出哪部分是首屏、哪部分是懒加载。这比任何文档都实在。

一个重要原则:注释不要写规则本身的内容。比如不要写 # 这条规则是HSPL... 这种废话,而是写业务意图:# 首页Feed流核心类。这样当规则需要调整时,你能快速判断是否该删掉它。

基本规则结构:描述符写法详解

规则行中的目标描述符(也就是类或方法)有一套严格的格式。我见过最多的问题就是把类路径写成带点号的形式(比如 com.example.MyClass),但 profile 要求用 JVM 内部描述符 —— 就是用 / 代替 .,并且类名后面加 ;。方法还要加上括号里的参数类型和返回值。

类型 正确的 prof 描述符 错误的写法
类 Lcom/example/myapp/HomeActivity; com.example.myapp.HomeActivity
方法 Lcom/example/myapp/util/FormatUtil;->formatDate(Ljava/util/Date;)Ljava/lang/String; FormatUtil.formatDate(Date)
内部类 Lcom/example/myapp/MainActivity$InnerFragment; com.example.myapp.MainActivity.InnerFragment

为什么会这样?因为 ART 编译器的内部解析器直接采用 JVM 格式。如果你写错了,这条规则会被静默忽略。嗯,我之前排查问题时,发现 prof 文件里有一半规则都不生效,原因就是少写了分号。

一个小技巧:用 javap 或者 Android Studio 的「Copy Reference」功能(右键方法 → Copy Reference),可以直接得到准确的描述符字符串。省得你手敲。

避坑指南:写 prof 最常见的三个坑

  1. 包名声明必须写在第一行,且不能加操作码。 我见过有人写 HSPLcom.example.myapp 当包名声明——编译直接报错。包名行只写包名字符串,前面不能有 L 也不能有 ;。
  2. 规则顺序不重要。 ART 会重新排序,所以你可以按业务模块组织,不用按字母序。
  3. 不要写太多规则。 我见过一个项目写了 3000 条规则,结果文件体积超过 100KB,编译时间和运行时内存都受影响。控制在 500 条以内比较安全。

最后再啰嗦一句:写完后别忘了在 build.gradle 里开启 profile 集成。不然你写得再漂亮,也进不了 APK。