第4章:编写自定义Baseline Profile规则
Baseline Profile 的核心价值在于「让 AOT 编译器知道该优化什么」。上一章我们学习了自动生成规则,但在生产环境中,自动规则往往不够精准:启动路径类可能遗漏、第三方库热点未被覆盖、冗余规则占用编译预算、多条规则冲突导致优化失效。本章从四个维度讲解如何编写高质量的自定义规则,确保每条规则都能转化为真实的启动加速。
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;
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
避免冗余的核心原则是「精准打击」:
- 优先使用
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: 未找到对应方法(可能已被混淆)
-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 | 可接受 |