第4章:编写自定义Baseline Profile规则

Baseline Profile 的核心价值在于「让 AOT 编译器知道该优化什么」。上一章我们学习了自动生成规则,但在生产环境中,自动规则往往不够精准:启动路径类可能遗漏、第三方库热点未被覆盖、冗余规则占用编译预算、多条规则冲突导致优化失效。本章从四个维度讲解如何编写高质量的自定义规则,确保每条规则都能转化为真实的启动加速。

自定义 Baseline Profile 规则编写流程 ① 识别启动路径类 分析冷启动 Trace 文件 标记 HSPL/HSP 核心类 ② 分析常用库依赖 梳理 Retrofit/OkHttp/Glide 提取启动路径热点方法 ③ 编写规则文件 按 HSPL/HSP/WARM 语法 写入 baseline-prof 文件 ④ 规则冗余检查 删除重复规则 通配符合并同类项 ⑤ 冲突检测处理 优先级排序 HSPL→POST_COLD 排除低效冲突规则 ⑥ 编译验证测试 AOT 编译并检查命中率 ProfileVerifier 验证
💡 规则编写原则:每条规则都对应一个「启动路径上的真实调用」。优先用 Trace 工具(Perfetto / Macrobenchmark)采集冷启动调用栈,再转化为规则,避免凭空猜测。

4.1 启动路径类标记

启动路径类是指应用冷启动过程中,从 Application.onCreate 到第一个 Activity.onResume 完成之间被调用的所有类。标记的目的是告诉 AOT 编译器:「这些类和方法必须在 dex2oat 期间编译为机器码」。

规则文件位于 src/main/baseline-prof,每行一个目标。标记级别决定了优先级:

标记符 级别 编译时机 典型场景
HSPL 最高 冷启动路径前 50% Application.onCreate、Activity.onCreate
HSP 高 冷启动路径前 100% Activity.onResume、Fragment 生命周期
WARM 中 温启动阶段 列表渲染、网络请求预热
POST_COLD 低 冷启动完成后延迟编译 图片解码、动画框架初始化

标记启动路径类的标准写法(以 baseline-prof 文件为例):

# 核心启动路径 — HSPL 级别
HSPLcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/example/app/MainActivity;->onStart()V
HSPLcom/example/app/MainActivity;->onResume()V
HSPLcom/example/app/App;->onCreate()V

# 次核心启动路径 — HSP 级别
HSPcom/example/app/SplashActivity;->onCreate(Landroid/os/Bundle;)V
HSPcom/example/app/MainFragment;->onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View;
HSPcom/example/app/HomeViewModel;->init()V

# 温启动路径
WARMcom/example/app/DetailActivity;->onCreate(Landroid/os/Bundle;)V
WARMcom/example/app/NetworkClient;->warmUp()V
🔍 标记技巧:使用通配符 * 标记类的所有方法,适合工具类或初始化类。
HSPLcom/example/app/Initializer;->*   # 标记 Initializer 所有方法
但通配符会增大编译体积,仅当该类所有方法都在启动路径上时才使用。

4.2 常用库预编译

第三方库在冷启动时占据大量时间。通过预编译库中的热点方法,可以省去解释执行的开销。以下是 Android 生态中最常被预编译的库及其规则示例:

库 预编译目标 建议标记级别
OkHttp 请求创建、连接池、DNS 解析 HSP
Retrofit 动态代理创建、ServiceMethod 构建 HSP
Glide Engine 加载、缓存查找、解码 HSPL
Gson / Kotlinx Serialization 反序列化构造器、类型适配器 WARM
Firebase / Crashlytics 初始化链、事件上报 POST_COLD

具体规则示例:

# OkHttp 预编译
HSPLokhttp3/OkHttpClient;-><init>(Lokhttp3/OkHttpClient$Builder;)V
HSPLokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
HSPokhttp3/RealCall;->execute()Lokhttp3/Response;
HSPokhttp3/RealCall;->enqueue(Lokhttp3/Callback;)V
HSPokhttp3/ConnectionPool;-><init>(IJLjava/util/concurrent/TimeUnit;)V

# Retrofit 预编译
HSPretrofit2/Retrofit;->create(Ljava/lang/Class;)Ljava/lang/Object;
HSPretrofit2/ServiceMethod;->parseAnnotations(Lretrofit2/Retrofit;Ljava/lang/reflect/Method;)Lretrofit2/ServiceMethod;
HSPretrofit2/OkHttpCall;-><init>(Lretrofit2/ServiceMethod;Ljava/lang/Object;)V

# Glide 预编译
HSPLcom/bumptech/glide/Glide;->init(Landroid/content/Context;)V
HSPLcom/bumptech/glide/Glide;->with(Landroid/content/Context;)Lcom/bumptech/glide/RequestManager;
HSPcom/bumptech/glide/load/engine/Engine;->load(Lcom/bumptech/glide/load/engine/EngineKey;)Lcom/bumptech/glide/load/engine/EngineResource;
⚠️ 预编译库的陷阱:不要无脑复制库的所有方法。每个库只预编译「冷启动路径上确实被调用」的方法。多余的规则会增大 .oat 文件体积,增加磁盘 I/O,反而拖慢启动。建议用 Macrobenchmark 的 ContextAwareRule 采集真实调用栈后再编写。

4.3 避免冗余规则

冗余规则是 Baseline Profile 最常见的质量问题。它们不会导致功能错误,但会浪费编译预算、增大安装包体积、延长 dex2oat 耗时。三类典型冗余及消除策略:

冗余类型 表现 消除策略
条目重复 同一个方法被多条规则覆盖 用 sort -u 去重;合并同类项
通配符膨胀 ClassA;->* 覆盖了大量未被调用的方法 限制通配符范围,只标记实际调用的方法
跨库冗余 不同库包含相同依赖(如 OkHttp 的公共基类) 统一使用 HSP 级别,避免高优先级重复

去重操作流程:

# 1. 对现有规则文件排序去重
sort src/main/baseline-prof | uniq > baseline-prof.clean

# 2. 统计规则数量
wc -l baseline-prof.clean

# 3. 检查通配符展开后的实际方法数(使用 prof-merger 工具)
java -jar prof-merger.jar --check-expansion baseline-prof.clean

# 4. 手动审查 TOP 20 最大类,拆分通配符
grep "\-\>*" baseline-prof.clean | awk -F';' '{print $1}' | sort | uniq -c | sort -rn | head -20
🎯 冗余消除目标:规则数量控制在 200–400 条之间,通配符展开后的方法数不超过 8,000。超出这个范围,AOT 编译的收益会边际递减,甚至变负。

避免冗余的核心原则是「精准打击」:

  • 优先使用 HSPL + 具体方法签名,而非 HSP + 通配符。
  • 对同一类的多个方法,用多行精确签名代替 ;->*。
  • 定期用 ProfileVerifier 检查规则命中率,移除命中率低于 10% 的规则。

4.4 规则冲突处理

当多个规则文件合并(例如项目自身的 baseline-prof 加上依赖库的 baseline-prof ),或者同一方法被不同级别标记时,就会产生冲突。冲突处理机制决定了最终哪条规则生效。

冲突类型与解决规则:

冲突场景 示例 解决策略
同一方法,不同级别 规则 A: HSPL → onCreate
规则 B: HSP → onCreate
高级别覆盖低级别:HSPL 生效,HSP 被忽略
同一方法,同级别,不同文件 主模块: HSP → init()
库模块: HSP → init()
后者覆盖前者:按模块依赖顺序,后加载的生效
通配符与精确签名 通配符: ClassA;->*
精确: ClassA;->foo()V
精确签名优先:foo() 按精确规则,其他方法按通配符
包含与排除冲突 规则包含 ClassB,排除规则排除 ClassB 排除优先级高于包含:最终排除 ClassB

在 build.gradle 中配置冲突决策:

// app/build.gradle.kts
android {
    baselineProfile {
        // 自动合并依赖库的 baseline-prof 文件
        mergeDependencies = true

        // 冲突时优先使用主模块规则
        conflictResolution = "preferAppModule"

        // 排除特定依赖的规则
        excludes += listOf(
            "com/example/unused/library/**"
        )
    }
}

当手动处理冲突时,可以使用 prof-merger 工具查看合并后的规则优先级:

# 合并所有 baseline-prof 文件并输出优先级报告
prof-merger \
    --input app/src/main/baseline-prof \
    --input library/src/main/baseline-prof \
    --output merged-baseline-prof \
    --report conflict-report.txt

# 查看冲突条目
grep "CONFLICT" conflict-report.txt
🔧 冲突处理最佳实践:
  • 在项目根目录维护一个 baseline-prof-override 文件,专门存放需要覆盖库规则的条目。
  • 冲突报告中的 CONFLICT 条目应该人工审查,确保生效的规则符合预期。
  • 使用 @RequiresOptimization(HSPL) 注解可以提升标记优先级,适合在代码中直接声明热点。

注解方式声明规则(替代手动编写文件):

// Kotlin 示例:使用 @RequiresOptimization 注解标记启动路径类
@RequiresOptimization(HSPL)
class AppInitializer {
    fun init() {
        // 初始化逻辑
    }
}

// 也可以标记整个包
@RequiresOptimization(HSP)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class StartupPath

注解方式会在编译期自动生成对应规则,适合与项目代码强绑定的场景。但对于第三方库,仍需手动编写规则文件。

4.5 规则验证与调试

编写完成后,必须验证规则是否真正生效。Android 提供了 ProfileVerifier 和 baseline-profile-gradle-plugin 两种验证手段。

使用 ProfileVerifier 检查命中率:

// build.gradle.kts
dependencies {
    debugImplementation("androidx.profileinstaller:profileinstaller:1.3.1")
    debugImplementation("androidx.profileinstaller:profileverifier:1.0.0")
}
// 在冷启动完成后获取验证报告
adb shell am broadcast -a androidx.profileinstaller.action.VERIFY_PROFILE \
    -p com.example.app

// 查看日志输出
adb logcat -s ProfileVerifier

// 典型输出
// ProfileVerifier: Compiled methods: 342 / 400 (85.5%)
// ProfileVerifier: Missed methods: 58
// ProfileVerifier: Top missed:
//   com/example/app/UnusedClass;->foo()V  (never called)
//   okhttp3/internal/connection/ConnectInterceptor;->intercept()V  (cold path)

Gradle 编译期验证:

# 在 release 构建中启用 profile 验证
./gradlew :app:assembleRelease -Pandroid.enableProfileVerification=true

# 构建完成后检查验证报告
cat app/build/reports/profile-verification/verification-report.txt

# 报告会列出每条规则的编译状态
# - COMPILED: 成功编译为机器码
# - SKIPPED:  被冲突规则覆盖
# - MISSING:  未找到对应方法(可能已被混淆)
⚠️ 混淆对规则的影响:如果使用 ProGuard / R8,方法签名在混淆后会改变。规则文件必须写在混淆之前,或者使用 -keep 保留启动路径类的方法名。建议在 proguard-rules.pro 中:
-keep class com.example.app.** { *; }
-keep class okhttp3.** { *; }
-keep class retrofit2.** { *; }
否则规则会在混淆后全部失效。

性能对比验证(A/B Test):

指标 无规则基线 有自定义规则 提升幅度
冷启动时间 (P50) 1240 ms 980 ms 21%
冷启动时间 (P99) 2100 ms 1650 ms 21.4%
AOT 编译方法数 0(默认) 342 —
APK 大小增量 — +1.8 MB 可接受
📊 验收标准:冷启动时间提升 ≥15%,规则命中率 ≥85%,APK 增量 ≤3 MB,且无新增 ANR 或崩溃。如果命中率低于 70%,需要重新审查规则与真实调用栈的匹配度。