Android 冷启动基线画像落地:Macrobenchmark、Baseline Profile 与安装后预编译

作者: Android学习网 分类: Android平台开发 发布时间: 2026-03-21 21:07

## 问题

一、`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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注