4. Application.onCreate 耗时定位与裁剪
为什么 Application.onCreate 是启动耗时的“重灾区”
冷启动时,进程从 Zygote fork 出来后,系统会依次创建 Application 对象并回调 onCreate(),然后才创建首个 Activity。因此 Application.onCreate() 中任何同步耗时都会直接叠加到用户可见的首帧之前。常见的高耗时来源包括:
- 第三方 SDK 初始化(统计、推送、崩溃上报、地图、支付等)
- 本地数据库/SharedPreferences 的首次读写
- 全局配置解析、字体加载、图片预解码
- 反射扫描、注解处理、类加载
定位思路是:先量化每个初始化任务的耗时,再判断哪些必须同步、哪些可以延迟到首帧之后。下面这张图展示了从进程创建到首帧的完整时间轴,以及同步初始化与延迟初始化在时间轴上的位置差异。
用耗时埋点量化每个初始化任务
不要凭感觉判断“哪个慢”,要用 SystemClock.uptimeMillis() 或 System.nanoTime() 对每个初始化任务做包裹计时。下面是一个可直接复用的轻量计时工具类:
object StartupTimer {
private val records = mutableListOf<Pair<String, Long>>()
inline fun <T> measure(tag: String, block: () -> T): T {
val start = SystemClock.uptimeMillis()
val result = block()
val cost = SystemClock.uptimeMillis() - start
records.add(tag to cost)
Log.d("StartupTimer", "$tag = ${cost}ms")
return result
}
fun dump(): String =
records.sortedByDescending { it.second }
.joinToString("\n") { "${it.first}: ${it.second}ms" }
}
在 Application.onCreate() 中包裹每个初始化调用:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
StartupTimer.measure("CrashSDK") {
CrashReporter.init(this)
}
StartupTimer.measure("StatsSDK") {
StatsAgent.init(this)
}
StartupTimer.measure("SharedPref") {
getSharedPreferences("config", MODE_PRIVATE)
.getString("key", null)
}
StartupTimer.measure("PushSDK") {
PushService.init(this)
}
StartupTimer.measure("MapSDK") {
MapSdk.init(this)
}
Log.d("StartupTimer", "===== 启动耗时汇总 =====\n${StartupTimer.dump()}")
}
}
埋点原则:每个
measure 块只包裹一个独立初始化任务,不要嵌套或合并,否则无法区分耗时归属。建议在 Debug 包中开启,Release 包中通过编译开关关闭日志输出。
判断哪些任务可以延迟
拿到耗时数据后,按以下标准分类:
| 判断维度 | 必须同步(留在 onCreate) | 可以延迟(首帧后执行) |
|---|---|---|
| 首帧是否依赖 | 崩溃上报、全局配置、主题资源 | 推送注册、地图定位、广告预加载 |
| 用户是否立即感知 | 影响首屏内容展示的初始化 | 后台服务、非首屏功能 |
| 是否可异步 | 必须在主线程完成的轻量操作 | 可放入子线程或 IdleHandler 的任务 |
延迟执行的实现方式
推荐使用 Looper.getMainLooper() 配合 MessageQueue.IdleHandler,在首帧渲染完成、主线程空闲时执行延迟任务:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 必须同步的:崩溃上报(首帧前需要保护)
StartupTimer.measure("CrashSDK") {
CrashReporter.init(this)
}
// 延迟到首帧后执行
Looper.getMainLooper().queue.addIdleHandler {
StartupTimer.measure("PushSDK") {
PushService.init(this)
}
StartupTimer.measure("MapSDK") {
MapSdk.init(this)
}
false // 返回 false 表示执行一次后移除
}
}
}
注意:
IdleHandler 在主线程空闲时执行,如果延迟任务本身耗时较长,仍可能造成后续帧卡顿。对于耗时超过 50ms 的任务,建议放入子线程或使用 ExecutorService 异步执行,并确保线程安全。
验证启动收益
优化前后分别用 adb shell am start -W 测量冷启动耗时,对比 TotalTime 字段:
# 优化前
adb shell am start -W -n com.example.app/.MainActivity
# TotalTime: 820ms
# 优化后(延迟 PushSDK + MapSDK)
adb shell am start -W -n com.example.app/.MainActivity
# TotalTime: 640ms
同时观察 Logcat 中 StartupTimer 的输出,确认:
- 同步任务耗时总和是否下降
- 延迟任务是否在首帧之后才执行(可通过
Choreographer.postFrameCallback打点对比) - 首帧渲染时间是否提前
示例数据说明:以上耗时数值(320ms、180ms、820ms、640ms)均为教学示例,实际数值取决于设备性能、SDK 版本和初始化内容。请以自己项目中的实测数据为准。