Android 冷启动基线画像落地:Macrobenchmark、Baseline Profile 与安装后预编译
## 问题
一、`Application` 初始化越堆越多时,最容易看到的不是一次大卡顿,而是新安装首启慢、升级后首开慢、低端机偶发超时。Android 冷启动基线画像的价值,不是追一个漂亮平均值,而是把 `measureRepeated`、安装后预编译、真实首启路径绑在一起,先锁住可重复的启动下限。
二、如果现场只看 Profiler,一般只能知道慢,却不知道慢在 dex 解释执行、类加载过散,还是首帧前同步初始化太多。这里先把测量入口固定,再决定拆初始化、补 Baseline Profile。
三、这类问题常见于多模块工程、首页依赖重、埋点和网络 SDK 都在 `onCreate` 抢时段的项目。先建立基线,再去谈优化。
## 方案
一、先把启动测量从业务工程里独立出来,单独建 `:benchmark` 模块,用 `MacrobenchmarkRule` 固定 `StartupMode.COLD`。这样每次回归都能拿到同一种输入,不会被缓存和后台残留干扰。
二、再把 Baseline Profile 生成链路接上,让安装后预编译覆盖首页真实启动路径。不要追求一次把所有页面都写进 profile,先把 `MainActivity`、首屏列表、首批仓库初始化覆盖住,更稳。
三、最后补两个兜底:把首启阶段的同步任务显式分层,基线产物接进 CI。后面有人加重初始化,也会先在基准测试里暴露。
## 示例代码
1、Benchmark 模块依赖
plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
id("androidx.baselineprofile")
}
android {
namespace = "com.example.benchmark"
targetProjectPath = ":app"
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.4")
implementation("androidx.test.uiautomator:uiautomator:2.3.0")
}
2、冷启动基准测试
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startupCold() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
iterations = 8,
startupMode = StartupMode.COLD,
setupBlock = { pressHome() }
) {
startActivityAndWait()
}
}
3、Baseline Profile 生成入口
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generate() = baselineProfileRule.collect(packageName = "com.example.app") {
pressHome()
startActivityAndWait()
device.waitForIdle()
}
}
## 注意点
一、不要把预热逻辑写进基准测试本身。只要测试代码提前打开页面或预先拉数据,最终得到的就不是冷启动画像,而是人为修饰后的数字。
二、不要只看均值。首启优化更该看 `P50`、`P90` 和低端机样本。平均值下降但尾部更差,线上体感通常不会变好。
三、不要把所有初始化都粗暴扔到异步线程。主线程时间降了,未必代表首屏可用更早;如果异步任务又阻塞首帧,就只是把慢点挪了位置。
## 报错与排查
1、`No target package found`
这个报错多数不是库有问题,而是 `targetProjectPath`、`applicationId` 或待测包没正确安装。先确认 benchmark APK 与 app APK 同版本,再核对 Gradle 配置是否真指向被测模块。
./gradlew :app:assembleRelease :benchmark:assembleAndroidTest
adb shell pm list packages | findstr example.app
adb shell cmd package resolve-activity --brief com.example.app
2、Profile 生成了但未生效
如果测试跑完了、产物也有了,但首启时间没变化,优先怀疑 profile 没被打进正式包,或安装后没有触发预编译。此时要把产物位置、安装方式和编译状态一起核对。
./gradlew :app:generateBaselineProfile
adb shell dumpsys package dexopt | findstr /I example.app
adb shell cmd package compile -m speed-profile -f com.example.app
## 可运行片段
一、下面给一个最小接线版本,目的是先把启动链路量出来,再决定是否深拆初始化。只要这套片段在测试机上稳定跑通,团队后续每次改首页时都能用同一把尺子回归。
1、应用侧延迟初始化
class App : Application() {
override fun onCreate() {
super.onCreate()
StartupTracer.mark("app_on_create")
ProcessLifecycleOwner.get().lifecycle.addObserver(AppStartupObserver())
}
}
2、最小埋点工具
object StartupTracer {
private val points = linkedMapOf<String, Long>()
fun mark(name: String) {
points[name] = SystemClock.elapsedRealtime()
}
fun dump(): String = points.entries.joinToString(" | ") { entry -> "${entry.key}=${entry.value}" }
}
3、本地执行命令
./gradlew :benchmark:connectedCheck
adb logcat -d | findstr /I "app_on_create StartupTracer"
adb shell am force-stop com.example.app
