3、核心启动指标:onCreate、onStart、onResume、onWindowFocusChanged

Activity 启动耗时,说白了就是用户点下图标到看到完整可用界面之间的那段时间。我做了这么多年性能优化,发现很多人只盯着 onCreate 算,这其实不够。真正的核心指标有四个:onCreate、onStart、onResume、onWindowFocusChanged。每个节点都有它独特的意义,埋点位置差一点,数据差十万八千里。

4.1 四个生命周期回调的定位

先看一张整体流程图,把四个节点串起来。

Activity 启动核心指标节点时序图 Activity 启动核心指标 & 埋点时机 时间 onCreate 布局创建 & 数据准备 onStart 界面可见但不可交互 onResume 可交互,但未完成绘制 onWindowFocusChanged true:首帧绘制完成 等待布局 & 绘制 常见埋点组合 • 冷启动总耗时 = onWindowFocusChanged - Application start • 页面绘制耗时 = onWindowFocusChanged - onResume • 业务初始化耗时 = onResume 内最早日志 - onCreate 用户体验感知:onWindowFocusChanged 才是用户看见完整界面的时刻

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 入口打 T1
  • onResume 入口打 T2
  • onWindowFocusChanged(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,但用户依然觉得启动慢,其实就是没关注渲染那一块。