第一章:卡顿现象识别——UI线程Blocked、帧率骤降、用户感知延迟

卡顿是用户对 App 流畅度的最直观负面体验。本章通过 现象识别 → 数据采集 → 原因定位 → 修复动作 → 复测 → 预防 六步闭环,帮助你建立从症状到根因的系统性排查框架。我们先从最常见的三种现象入手。

1. 现象识别:三类典型卡顿表现

现象本质用户感知典型场景
UI线程 Blocked主线程被长时间占用(>16.67ms)点击无响应、页面冻结主线程做文件读写、网络请求、复杂计算、同步锁等待
帧率骤降单帧渲染耗时>16.67ms,掉帧或跳帧滑动不跟手、动画撕裂布局嵌套过深、过度绘制、频繁GC
用户感知延迟从用户操作到反馈间隔>100ms操作与响应脱节、丢帧感协程挂起后恢复慢、消息队列积压
关键判断规则: 通过 Logcat 观察 Choreographer 是否输出 Skipped X frames!,该日志意味着主线程连续阻塞超过16.67ms数量级的帧数。若出现 InputEventReceiver: Timeout 则说明输入响应超时(通常>5ms即被系统标记)。

2. 数据采集:锁定卡顿证据

在排查前应收集以下三类数据,缺一不可:

  • Systrace / Perfetto 抓取主线程调度与帧边界: 观察 surfaceflinger 的 VSYNC 驱动、主线程上的 Choreographer.doFrame 和 InputDispatcher 事件。
  • BlockCanary 或手动 Looper.print 日志: 设置 Looper.getMainLooper().setMessageLogging() 监控每条消息耗时。
  • Choreographer 帧回调耗时: 通过 Choreographer.getInstance().postFrameCallback 计算相邻帧间隔。
最小采集命令集(Android 10+):
adb shell perfetto -t 10s -o /data/misc/perfetto-traces/trace.perfetto-text \
--txt /system/etc/perfettocfg/proto/cfg/perfetto_config.pbtx
通过 adb pull /data/misc/perfetto-traces/trace.perfetto-text 导出后使用 perfetto.dev 可视化分析。

3. 原因定位:从现象到根因的故障树

下图展示了卡顿排查的故障树结构,每个叶子节点对应一种常见根因。排查时应从浅层现象向下逐级追溯。

卡顿排查故障树(第一层:现象 → 第二层:数据 → 第三层:根因) 卡顿现象 UI线程Blocked 帧率骤降 用户感知延迟 Systrace/Perfetto BlockCanary/MsgLog Choreographer回调 协程调度切主线程 布局/绘制性能 锁竞争/消息积压 GC/IO/计算过重 修复:withContext(Dispatchers.IO/Default) 等 修复:Layout Inspector / 锁优化 / 协程限流 复测:重新抓帧率/Perfetto,验证不再掉帧;预防:编码规范+CI检测+协程调度器使用指南

4. 修复动作:典型问题快速修复指南

依据上述故障树定位到根因后,按以下优先级执行修复:

  • 协程调度误用: 将 Dispatchers.Main 的耗时操作替换为 Dispatchers.IO (文件/网络) 或 Dispatchers.Default (CPU密集) 并用 withContext 包裹。
  • 布局过度绘制: 使用 Layout Inspector 移除冗余 View 层级,减少触发 Measure/Layout 次数。
  • 锁竞争: 使用 Mutex (协程版本) 替代 synchronized,或改用单线程执行器。
  • 高频GC: 减少临时对象分配,对 RecyclerView 使用 ViewHolder+复用池。
// 错误示例:主线程做网络请求
GlobalScope.launch(Dispatchers.Main) {
    val data = fetchFromNetwork()   // 直接在主线程执行IO,阻塞!
}

// 正确示例:IO调度器
GlobalScope.launch(Dispatchers.Main) {
    val data = withContext(Dispatchers.IO) { fetchFromNetwork() }
    updateUI(data)
}

5. 复测:验证修复效果

使用与数据采集阶段相同的工具再次抓取性能数据:

  • 确保 Choreographer 不再输出 Skipped frames 日志。
  • Perfetto 中 main 线程上的 doFrame 间隔全数低于 16.67ms。
  • 手动滑动场景帧率稳定在 55~60fps。

6. 预防:建立卡顿防御机制

  • 编码阶段: 禁止在主线程使用 Thread.sleep()、File.xxx()、URLConnection;Kotlin 协程必须使用具名 CoroutineScope(Dispatchers.Main) 并保持调度器显式。
  • CI集成: 在自动化测试中插入 androidx.benchmark:benchmark-macro-junit4 的帧率断言;检测到单帧超过 16.67ms 则标记失败。
  • 运行时监控: 接入 BlockCanary 或字节码插桩,主线程单条消息超过 200ms 自动上报堆栈到 APM 平台。
避坑要点: 不要仅依赖 logcat 中就出现一次 Skipped frames 即判断系统卡顿——偶现一次可能来自系统服务抖动。连续 3 帧以上丢帧且用户操作无法回放,才视为问题。建议在 onFrameCallback 累积记录 lastFrameTime 偏差超过 30ms 时触发上报。