写在前面
Jetpack Compose 应用在冷启动、导航首帧、滚动列表等场景中会受 JIT/AOT 状态影响------早期帧掉得越多,用户体感越差。本文带你了解 Now in Android 项目的 benchmarks 模块,聊聊 Baseline Profile 的原理、如何编写 Macrobenchmark,以及实际收益。
一、为什么要关心 Benchmark 与 Baseline Profile?
| 痛点 | 解法 |
|---|---|
| 冷启动慢、首帧卡顿 | Baseline Profile 在安装期预编译关键路径 |
| "感觉快了"没有数据支撑 | Macrobenchmark 量化启动耗时、帧稳定性 |
| JIT 暖机阶段体验差 | 预编译减少运行时 JIT 抖动 |
二、Baseline Profile 原理
2.1 工作流程
生成 Profile ──▶ 合入 app 模块 ──▶ 安装期注入 ART ──▶ 部分 AOT 预编译
- Profile Installer (prod/release 构建默认启用)会在 app 安装阶段把
baseline-prof.txt注入 ART Profile。 - ART 根据 Profile 对命中的类/方法做 Partial AOT,减少运行时 JIT 开销。
- 生成 Profile 依赖 Macrobenchmark 跑真机/模拟器场景,提取热点方法签名。
2.2 为什么能让 Compose 冷启动变顺?
- 安装期预编译:关键路径提前优化,冷启动和首帧更稳。
- 数据来源:来自真实或模拟的 CUJ(Critical User Journey),覆盖启动 + 渲染 + 交互。
- 运行时收益:减少"前几秒越来越顺"的暖机阶段;首屏列表滚动更稳定;过渡动画掉帧更少。
三、ForYouBaselineProfile 示例拆解
代码位于 benchmarks/.../baselineprofile/ForYouBaselineProfile.kt:
34:43:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/ForYouBaselineProfile.kt
@Test
fun generate() =
baselineProfileRule.collect(PACKAGE_NAME, maxIterations = 10) {
startActivityAndAllowNotifications()
forYouWaitForContent()
forYouSelectTopics(true)
forYouScrollFeedDownUp()
}
| 要素 | 说明 |
|---|---|
BaselineProfileRule.collect |
启动待测 app、执行 CUJ、收集方法调用频率 |
maxIterations = 10 |
最多跑 10 轮,收敛后提前结束 |
| CUJ 设计 | 启动 → 等待内容 → 选话题 → 滚动,覆盖核心热点 |
小技巧:maxIterations 怎么选?
- UI 稳定时:3--5 次即可,加快生成速度。
- 复杂场景:可设 10,但注意设备发热导致指标漂移。
- 多场景拆分:启动、滚动、详情页分开收集,合并到主 Profile,便于定位回归。
四、ForYouActions:CUJ 动作封装
ForYouActions.kt 封装了 For You 场景的 UIAutomator 操作,保证 CUJ 可重复、可读。
4.1 等待内容加载
28:36:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt
fun MacrobenchmarkScope.forYouWaitForContent() {
device.wait(Until.gone(By.res("loadingWheel")), 5_000)
val obj = device.waitAndFindObject(By.res("forYou:topicSelection"), 10_000)
obj.wait(untilHasChildren(), 60_000)
}
- 二次确认:先等加载圈消失,再等列表有子项。
- 长超时:最多 60s,降低网络抖动导致的失败。
4.2 选择话题
42:90:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt
fun MacrobenchmarkScope.forYouSelectTopics(recheckTopicsIfChecked: Boolean = false) {
val topics = device.findObject(By.res("forYou:topicSelection"))
val horizontalMargin = 10 * topics.visibleBounds.width() / 100
topics.setGestureMargins(horizontalMargin, 0, horizontalMargin, 0)
// ... 循环选择至少 3 个话题
}
| 技巧 | 说明 |
|---|---|
setGestureMargins |
规避系统边缘手势干扰 |
recheckTopicsIfChecked |
多轮迭代时"点两次"刷新状态 |
childCount == 0 直接 fail |
避免生成空 Profile |
4.3 滚动列表
93:96:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt
fun MacrobenchmarkScope.forYouScrollFeedDownUp() {
val feedList = device.findObject(By.res("forYou:feed"))
device.flingElementDownUp(feedList)
}
快速下滑再上滑,触发列表布局/绘制热点路径。
4.4 主题切换(可选)
98:108:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt
fun MacrobenchmarkScope.setAppTheme(isDark: Boolean) {
when (isDark) {
true -> device.findObject(By.text("Dark")).click()
false -> device.findObject(By.text("Light")).click()
}
device.waitForIdle()
device.findObject(By.text("OK")).click()
waitForObjectOnTopAppBar(By.text("Now in Android"))
}
覆盖不同主题下的布局代码路径。
五、StartupBenchmark:量化冷启动
StartupBenchmark.kt 衡量冷启动耗时,对比不同编译/Baseline Profile 状态下的收益。
5.1 四种编译模式
45:57:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt
@Test fun startupWithoutPreCompilation() = startup(CompilationMode.None())
@Test fun startupWithPartialCompilationAndDisabledBaselineProfile() =
startup(CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1))
@Test fun startupPrecompiledWithBaselineProfile() =
startup(CompilationMode.Partial(baselineProfileMode = Require))
@Test fun startupFullyPrecompiled() = startup(CompilationMode.Full())
| 模式 | 说明 | 用途 |
|---|---|---|
| None | 纯 JIT,冷启动最慢 | 基线参照 |
| Partial + Disable | 部分 AOT,禁用 Baseline | 隔离 Profile 差异 |
| Partial + Require | 部分 AOT + Baseline | 真实发布态 |
| Full | 全量 AOT | 极限参考 |
5.2 测量逻辑
59:74:benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = BaselineProfileMetrics.allMetrics,
compilationMode = compilationMode,
iterations = 5,
startupMode = COLD,
setupBlock = {
pressHome()
allowNotifications()
},
) {
startActivityAndAllowNotifications()
forYouWaitForContent()
}
| 参数 | 说明 |
|---|---|
iterations = 5 |
同一配置跑 5 次,提升统计可靠度 |
startupMode = COLD |
冷启动,进程完全退出后重启 |
setupBlock |
每轮前退到桌面、处理通知权限 |
| 结束条件 | forYouWaitForContent() 等待内容就绪 |
六、实施步骤
bash
┌─────────────────────────────────────────────────────────────┐
│ 1. 准备环境 │
│ Android Studio 最新版 + AGP 8+ │
│ 确保 Profile Installer 启用(默认 on) │
├─────────────────────────────────────────────────────────────┤
│ 2. 生成 Baseline Profile │
│ ./gradlew :benchmarks:pixel6api31aospBenchmark │
│ 产物位于 benchmark/build/outputs/baseline-prof.txt │
├─────────────────────────────────────────────────────────────┤
│ 3. 合入 app 模块 │
│ 拷贝到 app/src/main/baseline-prof.txt │
├─────────────────────────────────────────────────────────────┤
│ 4. 验证收益 │
│ 再跑 StartupBenchmark,对比各模式指标 │
├─────────────────────────────────────────────────────────────┤
│ 5. CI 集成 │
│ 放入可选 Job,大改动后跑一轮防回归 │
└─────────────────────────────────────────────────────────────┘
七、收益预期
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 冷启动 | ~700--900ms | ~400--600ms |
| 首帧稳定 | 明显掉帧 | 更平滑 |
| 滚动流畅度 | 暖机阶段卡顿 | 首滚即顺 |
具体数值因设备/构建差异会有浮动,但趋势一致。
八、维护小技巧
- 场景可重复:固定账号/数据,必要时禁用动画。
- 迭代次数适中:冷启动 5--8 次可得稳定均值。
- 分场景拆分:启动、滚动、详情页分测试,便于定位回归。
- 定期再生成:功能演进会改变热点路径,Profile 需随版本更新。
- 设备一致性:优先同型号真机。
结语
Baseline Profile + Macrobenchmark 是 Now in Android 这类 Compose 项目里性价比极高的性能优化手段:
- 把"安装即顺畅"交付给用户
- 把"可量化的性能"交给团队
快把 Benchmark 模块跑起来,看看你的启动时间能省下多少毫秒吧!