第1章 现象识别:丢帧、白屏、页面跳闪、用户感知卡顿

首次显示延迟是 ViewModel 在使用 Compose / Activity 冷启动或热启动场景下最常见的性能劣化表现。用户直观感受到的“点开页面后空转、内容闪现、滑动卡住”背后对应着精确的渲染管线指标。本章定义四种核心现象,建立可量化的识别标准,并为后续数据采集与定位提供故障排查锚点。

首次显示延迟现象识别故障树 首次显示延迟 丢帧 白屏 页面跳闪 用户感知卡顿 FrameDrop>3 冻帧>200ms 空白>500ms 无内容帧 亮暗跳变>2次 布局重排抖动 触摸响应>100ms 滚动掉帧>50% 诊断原则 任意一种现象出现即视为“首次显示延迟阳性”,需进入数据采集阶段。

1.1 现象识别

四种现象在 ViewModel 首次显示场景下有明确的 视觉特征 和 可测量边界。下表给出定义与快速判断规则:

现象 视觉特征 可测量边界 初步判断规则
丢帧 界面滑动或动画过程中出现明显停顿,画面跳跃 连续丢帧 ≥ 3 帧,单次冻帧 ≥ 200ms 使用 Profiler 观察 Fps 曲线低于 30fps 持续 300ms
白屏 进入页面后长时间显示白色或空白背景,无内容呈现 空白持续时间 ≥ 500ms 截取 Launch 时间戳,从 onStart 到第一帧绘制 ≥ 800ms
页面跳闪 页面内容先出现后消失或明暗交替闪烁 亮暗跳变 ≥ 2 次,或布局重排导致内容抖动 录制屏幕回放,逐帧检查亮度突变帧
用户感知卡顿 点击按钮或滑动列表时明显迟滞,响应不跟手 触摸响应延迟 ≥ 100ms,滚动帧率掉帧率 ≥ 50% 通过 FrameTiming 获取触摸事件到第一帧绘制间隔
快速排查口诀: “丢帧看曲线,白屏掐时间,跳闪录回放,卡顿量响应。”

1.2 数据采集

确认存在上述现象后,需要通过工具采集客观数据作为后续定位的依据。采集分两个层级:

  • 层级一:宏观指标采集(所有现象通用)
    • 使用 Android Studio CPU Profiler / Main Thread 记录首次进入页面的线程活动。
    • 使用 Frame Rendering Data (ADB 命令) 导出每帧耗时。
  • 层级二:现象专项采集
现象 采集命令/工具 关键指标
丢帧
adb shell dumpsys gfxinfo [package] framestats
Draw / Prepare / Process 阶段耗时 > 16ms 的帧数占比
白屏
adb shell am start -S -W [package]/[activity]
TotalTime 与 WaitTime 差值 ≥ 500ms
页面跳闪 录屏 + MediaMetadataRetriever 逐帧提取亮度直方图 连续 2 帧亮度均值跳变 > 30%
用户感知卡顿 Choreographer.getInstance().postFrameCallback 埋点 帧回调延迟 > 100ms 的累计次数
采集陷阱: 不要在 Debug 模式下采集首帧数据,JIT 与 GC 行为会掩盖真实问题。始终使用 adb shell setprop debug.layout true 保持 release 级别性能特征。

1.3 原因定位

四种现象的根源通常落在 ViewModel 初始化链路 的三个关键阶段上:

  • 阶段 A — ViewModel 实例化: 构造方法耗时、依赖注入 (Hilt/Koin) 图解析、SavedStateHandle 反序列化。
  • 阶段 B — 数据加载: Room / Network 首次查询、Flow 冷启动、协程调度器竞争。
  • 阶段 C — 首次 collect: StateFlow 初始值发射、重组范围过大、Compose 副作用执行。

使用以下 判断规则表 快速锁定嫌疑阶段:

现象 大概率嫌疑阶段 辅助判断日志键
丢帧 阶段 C — collect 触发大量重组 Compose:RecomposeCount > 500 次
白屏 阶段 B — 数据加载阻塞首帧 ViewModel:loadData duration > 400ms
页面跳闪 阶段 A — 实例化过程中状态覆盖 SavedStateHandle 恢复时间戳 > 200ms
用户感知卡顿 阶段 A + C 叠加 — 主线程竞争 Choreographer:skip > 3 帧

1.4 修复动作

基于定位结果执行对应修复。本章仅列现象层面的 快速止血动作,深入修复在第 4~8 章展开:

  • 丢帧 → 减少首次 collect 重组范围: 使用 derivedStateOf 或 snapshotFlow 缩小观察粒度,避免整个 Composable 重建。
  • 白屏 → 预加载 + 占位帧: 在 ViewModel 构造中提前触发数据加载,并用 Placeholder 组合项填充首帧。
  • 页面跳闪 → 稳定初始状态: 确保 ViewModel 在构造期间只发射一次初始态,使用 MutableStateFlow(initialValue) 避免 null → 值 跳变。
  • 用户感知卡顿 → 异步化触摸响应: 将点击/滑动事件中的 ViewModel 调用通过 CoroutineScope(Dispatchers.IO) 分发,不阻塞 Main 线程。
现场保留技巧: 在 ViewModel.onCleared() 中打印 init 到首次 collect 的总耗时,帮助回归时验证修复效果。

1.5 复测

修复后必须执行 对照复测,确保现象消除且不引入新问题:

  1. 冷启动复测: 使用 adb shell am force-stop [package] 杀死进程,重新启动并记录耗时。连续执行 5 次取中位数。
  2. 热启动复测: 按 Home 键后立即点击图标重新进入,检查跳闪与卡顿。
  3. 数据对比: 将修复前后的关键指标填入下表。
指标 修复前 修复后 通过标准
首帧耗时 (ms) ≥ 800 ≤ 400 降低 ≥ 50%
丢帧次数 (次/5s) ≥ 10 ≤ 2 丢帧率 ≤ 5%
用户感知延迟 (ms) ≥ 150 ≤ 50 ≤ 50ms

1.6 预防

从流程和代码规范层面建立防护,避免同类问题再次出现:

  • 代码审查检查项: ViewModel init 中不得包含网络请求、数据库写操作、文件 IO;上述操作必须通过 viewModelScope.launch 异步执行。
  • 性能看板: 在 CI 中集成 Macrobenchmark Startup 测试,设置首帧耗时阈值(如 < 500ms),超时即告警。
  • 风格指南条目: “ViewModel 首次 collect 不应触发超过 300 次重组”,通过 Compose Compiler Metrics 输出重组计数。
  • 定期巡检: 每迭代执行一次 dumpsys gfxinfo 导出帧率报告,存入版本对比库。
本章核心交付: 一张现象故障树 + 四类现象的量化边界 + 采集命令表 + 阶段定位规则 + 复测对照表。完成本章后,学员应能在 10 分钟内完成首次显示延迟的识别与初判。