Android 冷启动优化实战:从 3 秒到 800ms 的排查路径
线上反馈"App 打开慢"是最常见也最难量化的性能问题。这篇复盘一次真实优化过程:把冷启动从 3.1 秒压到 800ms 左右,方法比结果更值得抄。
一、先把"冷启动"讲清楚
冷启动 = 进程不存在 → 启动进程 → 加载 Application → 创建首个 Activity → 首帧渲染完成。
三个关键节点:
- T0:
am_proc_start系统开始拉进程 - T1:
Application.onCreate结束 - T2:首个 Activity
onWindowFocusChanged(true)触发
我们要压的是 T2 - T0。中间的每一步都可能是黑锅。
二、测量:不测量的优化都是玄学
只信一种数据:adb shell am start -W。
adb shell am start -W -n com.demo.app/.MainActivity
输出里关注:
ThisTime:Activity 启动耗时TotalTime:包含 Application 的总耗时WaitTime:AMS 层的等待时间
再叠加代码内打点:
class App : Application() {
override fun onCreate() {
val t0 = SystemClock.elapsedRealtime()
super.onCreate()
// ... 初始化
val cost = SystemClock.elapsedRealtime() - t0
Log.i("Startup", "App.onCreate cost=${cost}ms")
}
}
Activity 侧用 Choreographer.getInstance().postFrameCallback 或者 reportFullyDrawn() 抓首帧。
三、Application 层:三板斧
这轮优化里 Application 从 1.4s 降到 380ms,靠的是三件事。
1. 分级初始化
把 SDK 按"必须同步"/"可延后"/"可后台"三类拆开:
override fun onCreate() {
super.onCreate()
// 必须同步
initCrashReport()
initLogger()
// 主线程 idle 后跑
Looper.myQueue().addIdleHandler {
initAnalytics()
initImageLoader()
false
}
// 后台线程
Thread {
initPushSDK()
initABTest()
}.start()
}
关键点:IdleHandler 只跑主线程能容忍的活,别把耗时 SDK 塞进去,否则只是把卡顿从启动挪到首屏交互。
2. 延迟到首屏之后
用 App Startup 库或者手写 ContentProvider 触发器都行,核心是把非首屏依赖的初始化推到 Activity onResume 后:
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (!inited) {
window.decorView.post {
initSecondaryModules()
inited = true
}
}
}
}
3. 干掉隐式 ContentProvider 初始化
第三方 SDK 常用 <provider> 悄悄在启动阶段跑代码。检查方式:
adb shell dumpsys package com.demo.app | grep -A2 "Provider"
看到不必要的 Provider,用 tools:node="remove" 关掉,然后手动懒加载:
<provider
android:name="com.some.sdk.InitProvider"
android:authorities="${applicationId}.some.init"
tools:node="remove" />
这一步单独省了 220ms,很多 SDK 的 Provider 其实完全不需要跟启动绑定。
四、首屏 Activity:布局和数据都要瘦
Application 优化到位后,Activity 那 900ms 就成了瓶颈。
1. 布局层级和 measure/layout
用 Layout Inspector 或 hierarchyviewer 看首屏层级。这次踩到两个坑:
- 一个
ConstraintLayout里嵌了 5 层LinearLayout,重构成扁平 Constraint 后省 90ms include了一个巨大的通用头部布局,里面 90% 视图visibility=gone,改成ViewStub懒加载省 60ms
2. 主线程数据源要异步
首屏依赖的接口不要在 onCreate 里同步等。用 Flow + Loading 骨架屏:
class HomeViewModel : ViewModel() {
val state = MutableStateFlow<UiState>(UiState.Loading)
init {
viewModelScope.launch {
state.value = try {
UiState.Success(repository.load())
} catch (e: Exception) {
UiState.Error(e)
}
}
}
}
Activity 侧先渲染骨架,数据回来再刷。用户感知的"启动完成"不是数据完成,是首帧可见。
3. 主题闪屏别用 Activity
老代码里用 SplashActivity → MainActivity 跳转,光 Activity 创建就多花 150ms。改成 Theme 层的 windowBackground:
<style name="AppTheme.Launcher" parent="AppTheme">
<item name="android:windowBackground">@drawable/launch_bg</item>
</style>
MainActivity 用 Launcher 主题,super.onCreate 后立刻切回 AppTheme。省一个 Activity,稳赚。
五、验证和防回归
优化完不做防回归,三个月后必然被打回原形。加两道闸:
1. CI 里加启动耗时基线
用 am start -W 跑 20 次,取 P90,超过基线 15% 直接失败。这套脚本可以放到 GitHub Actions 或 Jenkins。
2. 线上监控
上报 Application.attachBaseContext 到 onWindowFocusChanged 的耗时,按机型分位统计。低端机 P90 才是真实体验,别只看平均值。
object StartupTracker {
private var t0 = 0L
fun mark() { t0 = SystemClock.elapsedRealtime() }
fun report(tag: String) {
val cost = SystemClock.elapsedRealtime() - t0
Analytics.log("startup", mapOf("tag" to tag, "cost" to cost))
}
}
六、复盘:哪些是"看起来很美"的优化
有几种做法要谨慎:
- 多 dex 优化 / MultiDex.install 提前:现在 minSdk ≥ 21 基本不用管
- StrictMode 关掉:只是把问题藏起来,别关,改成 release 关 debug 开
- 反射调用系统 API 跳过初始化:兼容性成本远大于收益
- 提前预加载类:意义不大,dex2oat 之后差异不明显
真正立竿见影的还是那三件:减少 Application 里的同步工作、首屏布局扁平化、用主题闪屏代替 SplashActivity。
小结
冷启动优化没什么魔法:测量 → 拆解 → 分级 → 防回归。别一上来就找玄学方案,先把 am start -W 跑起来,把 Application 里的初始化列个表,砍掉一半你就赢了一半。