3、核心启动指标:onCreate、onStart、onResume、onWindowFocusChanged
Activity 启动耗时,说白了就是用户点下图标到看到完整可用界面之间的那段时间。我做了这么多年性能优化,发现很多人只盯着 onCreate 算,这其实不够。真正的核心指标有四个:onCreate、onStart、onResume、onWindowFocusChanged。每个节点都有它独特的意义,埋点位置差一点,数据差十万八千里。
4.1 四个生命周期回调的定位
先看一张整体流程图,把四个节点串起来。
4.2 onCreate:页面起点,但别全压给它
onCreate 是 Activity 的第一个回调。我个人习惯在这里埋第一道时间戳,标记「界面开始构建」。
核心指标定义:从 Activity 创建到 onCreate 结束的时间差。通常包含 setContentView、findViewById、ViewModel 初始化、数据库加载等。
我的经验:别把所有 findViewById 堆在 onCreate 里,尤其列表页。我在一个电商项目里见过,8秒的启动耗时,onCreate 里占掉 5秒,就是因为 Adapter 在里面创建了三个 RecyclerView,每个都做了大量数据绑定。后来改用 onViewCreated 延迟加载,总启动降到 3.2 秒。
4.3 onStart:可见但不可触碰的关键
onStart 之后 Activity 对用户可见,但还不能触摸操作。很多人忽略这个节点,觉得它没什么用。其实,这里是最适合做「预加载」和「统计曝光」的地方。
| 场景 | onStart 埋点价值 | 避坑建议 |
|---|---|---|
| 快速启动 | 标记界面首次可见的时间 | 不要在 onStart 里做重量级操作 |
| 广告/埋点 | 记录页面曝光 | 避免多次回调,需做防重复 |
| 数据预取 | 发起网络请求、图片预加载 | 谨慎使用,确保不被 Fragment 状态干扰 |
我曾经掉进过一个坑:在 onStart 里直接调了 Retrofit 的同步请求,导致 onStart 到 onResume 之间卡了 800ms。你想想看,用户手机界面已经显示了,但手指点上去没反应,这体验多糟糕。后来改成异步,优先保证 onStart 快速返回。
4.4 onResume:可交互,但首帧还没画完
onResume 表示 Activity 进入前台,可以和用户交互。但要注意!这时候首帧可能还没渲染完成。说白了,用户看到的还是空白或者 Layout 未完全绘制的状态。
关键知识点:onResume 回调是在 Choreographer 开始排帧之前触发的。所以 onResume 结束的时间 ≠ 用户看到画面的时间。我习惯把 onResume 作为「业务准备完毕」的标记点,而非「UI 就绪」点。
4.5 onWindowFocusChanged(true):真正的首帧完成
这个回调是官方文档里明确说的「Window has gained focus」—— 当它为 true 时,表示当前窗口已完成所有测量、布局、绘制,并且 Display 驱动已经把帧送到了屏幕。这才是真正意义上的「用户看到完整界面」的时刻。
我的埋点方案:
- 在
Application.attachBaseContext打第一个时间戳 T0 onCreate入口打 T1onResume入口打 T2onWindowFocusChanged(true)打 T3- 冷启动总耗时 = T3 - T0
- 布局绘制耗时 = T3 - T2
- 业务初始化耗时 = T2 - T1
// 埋点示例
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "LaunchMetrics"
private var t0 = System.nanoTime()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val t1 = System.nanoTime()
Log.d(TAG, "onCreate: 耗时=${(t1 - t0) / 1_000_000}ms")
// 业务代码...
}
override fun onResume() {
super.onResume()
val t2 = System.nanoTime()
Log.d(TAG, "onResume: 耗时=${(t2 - t0) / 1_000_000}ms")
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
val t3 = System.nanoTime()
Log.d(TAG, "首帧完成: 总耗时=${(t3 - t0) / 1_000_000}ms, 绘制耗时=${(t3 - t2) / 1_000_000}ms")
}
}
}
4.6 各指标对比与建议
| 指标 | 含义 | 建议阈值 | 常见问题 |
|---|---|---|---|
| onCreate 耗时 | 布局创建 + 数据初始化 | < 300ms(冷启动) | JSON 解析、长列表绑定 |
| onStart 到 onResume | 可见至可交互的过渡 | < 100ms | 同步请求、SharedPreferences 批量写入 |
| onResume 到 onWindowFocusChanged | 布局渲染 + 首帧合成 | < 200ms | 过度绘制、View 层级过深、大图加载 |
| 冷启动总耗时 | 应用启动到首帧 | < 1.5s(优秀),< 3s(合格) | Application onCreate 初始化过多、ContentProvider |
注意:多 Fragment 页面中 onWindowFocusChanged 只有宿主 Activity 会回调。如果你想统计某个 Fragment 的首帧时间,需要用 ViewTreeObserver.addOnGlobalLayoutListener 或者 DrawCallback 自行计算。我踩过这个坑——在 Fragment 的 onResume 里埋点,结果发现数据比实际首帧早了 600ms,因为 Fragment 的 View 还没 attach 到 Window。
4.7 总结一句话
衡量 Activity 启动耗时的终极标准,就是 onWindowFocusChanged(true)。其他三个节点只是中间过程的诊断指标。记住,优化时要「最终结果导向」,而不是只看中间某一段的耗时。把 onResume 作为目标去优化,容易误判。我见过太多团队把 onCreate 优化到了 200ms,但用户依然觉得启动慢,其实就是没关注渲染那一块。