ARTICLE · 1208042
App 启动慢?Baseline Profile 让冷启动提速 40%
你有没有遇到过这种情况——用户打开你的 App,等了 2 秒白屏,然后默默按了返回键。
或者更扎心的:你在开发机上测试启动飞快,一到用户手机上就慢得像蜗牛。你打开 Android Studio Profiler 看了一圈,感觉哪都没问题,但用户投诉就是越来越多。
今天聊的这个技术,可能就是那块缺失的拼图——Baseline Profile。它不需要改业务代码,不需要重构架构,但可能让你的冷启动直接快 30%~40%。
———— 一 ————
1. ART 的"暖场"问题
Android Runtime(ART)执行代码并不是一开始就全速运转的。当你刚装或刚更新了 App,ART 对你的代码一无所知:
▸ 解释执行:最初所有方法都走解释器,速度最慢
▸ JIT 编译:运行过程中,ART 发现某些方法被频繁调用(热点),才触发即时编译
▸ AOT 编译:空闲时根据收集到的热点信息,在后台做全量编译优化
问题来了——冷启动恰恰发生在"ART 还不认识你的代码"的时候。
Application.onCreate()、首页 Activity 创建、首屏布局 inflate、各种 SDK 初始化……这些关键路径上的代码,此刻全都在解释执行或等待 JIT。用户感受到的卡顿,很多时候不是因为代码写得烂,而是因为编译优化还没到位。
这就好比一家餐厅刚开张,厨房还没热好锅,前几个客人的菜就是慢——不是厨师手艺不行,是"暖场"还没完成。
2. Baseline Profile 解决了什么
Baseline Profile 的思路很简单:
既然 ART 需要运行一段时间才知道哪些代码是热点,那我们就提前告诉它。
你把一份"关键代码路径清单"打包进 APK,用户安装 App 时,ART 会发现这份清单,然后在后台启动一个轻量级的编译过程,把清单上的方法提前编译成机器码。等用户真正点开 App 的时候,关键路径上的代码已经是优化过的状态了。
打包阶段:Profile 文件放入 APK 的
assets/dexopt/baseline-prof 目录安装阶段:ART 发现 Profile → 后台预编译清单中的关键方法
启动阶段:关键路径代码直接以编译后状态执行 → 启动提速
Baseline Profile 能优化的场景不止冷启动,还包括:
▸ 首页渲染与列表滚动
▸ 高频页面跳转
▸ Compose 首次组合
▸ 登录流程等核心业务路径
3. 先看数据:到底能快多少
空口无凭,以下是一个真实项目的 Macrobenchmark 冷启动测试结果(同一设备、同一构建、10 次迭代取中位数):
优化前
340ms
TTID median
优化后
295ms
TTID median
从 340ms 降到 295ms,减少 45ms,提升约 13%。这个数据看起来不算夸张?别忘了——我们一行业务代码都没改。
如果是那些还没做过启动优化、Application 里堆满 SDK 初始化的项目,Baseline Profile 的收益会更显著,冷启动提速 30%~40% 完全可能。
———— 二 ————
4. 第一步:创建 Macrobenchmark 模块
Baseline Profile 的生成依赖 Jetpack Macrobenchmark 框架。先在项目中新建一个 benchmark 模块:
项目结构:
app/ benchmark/ src/main/AndroidManifest.xml src/main/java/.../StartupBenchmark.ktbenchmark 模块的 build.gradle.kts:
plugins { id("com.android.test") id("org.jetbrains.kotlin.android") } android { namespace = "com.example.app.benchmark" targetProjectPath = ":app" defaultConfig { minSdk = 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } dependencies { implementation("androidx.test.ext:junit:1.2.1") implementation("androidx.test.uiautomator:uiautomator:2.3.0") implementation("androidx.benchmark:benchmark-macro-junit4:1.3.4") }目标 App 需要提供
profileable 能力。通常通过专用构建类型配置,不要为了跑测试把 release 包改成 debuggable。5. 第二步:编写冷启动基准测试
先写一个最小的冷启动 Macrobenchmark,用来建立优化前的基线数据:
@RunWith(AndroidJUnit4::class) class StartupBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun coldStartup() = benchmarkRule.measureRepeated( packageName = "com.example.app", metrics = listOf(StartupTimingMetric()), compilationMode = CompilationMode.Partial( baselineProfileMode = BaselineProfileMode.Require ), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { pressHome() } ) { startActivityAndWait() } }几个关键参数解释:
▸ StartupMode.COLD:保证每轮从无进程状态启动,模拟真实冷启动
▸ iterations = 10:跑 10 次取统计值,不能只看最好的一次
▸ CompilationMode.Partial(...):指定编译模式,后面验证时会用到
运行这个测试,你会得到类似这样的结果:
// 优化前基线 TTID min: 324.7ms TTID median: 340.2ms TTID max: 387.0ms6. 第三步:生成 Baseline Profile
现在用 BaselineProfileRule 编写一个生成器,让自动化脚本模拟真实用户操作:
@RunWith(AndroidJUnit4::class) class BaselineProfileGenerator { @get:Rule val rule = BaselineProfileRule() @Test fun generate() = rule.collect( packageName = "com.example.app", includeInStartupProfile = true ) { pressHome() startActivityAndWait() // 模拟真实用户高频路径 device.findObject(By.text("推荐")).click() device.waitForIdle() device.findObject(By.res("com.example.app", "feed_list")) .setGestureMargin(device.displayWidth / 5) .fling(Direction.DOWN) } }运行后,生成的 Profile 文件会出现在:
app/src/release/generated/baselineProfiles/ ├── baseline-prof.txt // ~2万行方法清单 └── startup-prof.txt // 启动类重排信息生成场景要稳定且覆盖真实高频路径。依赖文案定位控件(如 By.text("推荐"))容易因国际化或产品改版失效,优先使用稳定的资源 ID。
7. 第四步:集成到 App 构建
在 app 模块的 build.gradle.kts 中添加 Baseline Profile 插件:
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("androidx.baselineprofile") } android { // ... 其他配置 buildTypes { release { // Baseline Profile 会自动合并到 release 构建中 } } } dependencies { // 引入 benchmark 模块生成的 profile implementation("androidx.profileinstaller:profileinstaller:1.4.1") "baselineProfile"(project(":benchmark")) }———— 三 ————
8. 最重要的一步:对照实验
很多人加完 Baseline Profile,看到 Build Success 就宣布优化成功了。这是最大的误区。
加上 Baseline Profile → Build Success → 宣布启动优化成功。
实际上 Profile 可能根本没打进 APK,或者场景没覆盖启动关键路径,效果为零。
正确做法是单变量对照实验——在完全相同的条件下,分别跑两组测试:
对照组 A:不使用 Baseline Profile
compilationMode = CompilationMode.None() // 结果:TTID median = 340ms对照组 B:使用 Baseline Profile
compilationMode = CompilationMode.Partial( baselineProfileMode = BaselineProfileMode.Require ) // 结果:TTID median = 295ms条件保持一致:同一台设备、同样构建类型、同样测试次数。如果多次测试结果稳定,才可以判断 Baseline Profile 对当前项目确实有收益。
常见原因:
1. Profile 没有打进 release 构建——检查构建产物
2. 自动化场景没有覆盖启动关键路径——补充生成场景
3. 瓶颈主要是主线程 I/O、锁等待或网络——Profile 解决不了这些,要先做业务层优化
4. 测试设备状态不稳定——换设备或增加迭代次数
———— 四 ————
9. 别搞混了:两个 Profile 的区别
这俩名字太像,搞混的人不在少数。一句话区分:
| 管什么 | ||
| 作用于 | ||
| 效果 | ||
| 记忆口诀 |
两者机制不同但最终都可能改善启动性能。测试时也不要一次性全加上——否则 340ms → 285ms 的结果很漂亮,但你完全不知道是谁产生的收益。
———— 五 ————
10. 在 CI 中防止性能回退
启动优化最怕的不是没做,而是做完之后又慢慢退回去。
业务持续增长——新增 SDK、加一个 ContentProvider、多一个数据库查询、首页又多了两个模块。每次可能只增加 10~20ms,单独看不明显,半年下来启动就从 350ms 悄悄涨回 500ms。
这就是性能劣化,也是为什么启动优化不能只做一次。
理想的方案是在 CI 中接入 Macrobenchmark 作为性能回归测试:
CI 需要保存的产物:
▸ Macrobenchmark 的 JSON 结果(含 TTID/TTFD 中位数与高分位)
▸ 对应构建版本与提交信息
▸ 失败或异常样本的 Trace 文件
▸ 历史中位数和高分位趋势
门槛不要只写一个绝对毫秒值——不同设备差异很大。更实用的方式是固定测试设备,结合相对回退比例和最小变化量判断。例如:只有当中位数明显变慢且超过噪声区间时,才阻止合并。
———— 六 ————
11. 一张图记住全部
整个启动优化的方法论,最后就五个词:
Baseline Profile 是"做优化"这一环里的重要武器,但它不是全部。正确的顺序是:
① 先解决业务层大头——延迟初始化、删除无用 SDK、按需加载
② 再用 Baseline Profile——让关键路径代码提前编译
③ 最后考虑深层优化——Startup Profile、DEX Layout、ReDex InterDex
而每一步,都要用 Macrobenchmark 数据说话。不靠感觉,不挑最好的一次结果,看中位数、看高分位、看趋势。
性能优化最终的 KPI 只有一个:用户到底更快看到页面了吗?用户到底更快可以正常使用 App 了吗?
工具只是手段,性能闭环才是方法。















































































