Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ANR 与卡顿排查

ANR, 掉帧和 “页面感觉慢” 是不同问题. 阈值依赖触发类型, Android 版本和设备状态; 不要背一个固定秒数覆盖所有场景. 方法论, 指标口径与跨域取舍见 性能优化总览.

一, ANR 类型与版本条件

常见类型包括输入分发超时, BroadcastReceiver, Service, ContentProvider 和前台服务相关超时. 具体阈值与判定逻辑可能随 Android 版本, 前后台状态, CPU / 内存压力和组件类型变化, 排查时应以对应系统版本官方文档和 traces 为准.

回答面试题时不要只说 “主线程卡 5 秒就是 ANR”.更完整的表达是: 系统监控特定组件或交互的完成 deadline; 应用在 deadline 内没有响应时, 系统记录 ANR, 随后结合进程状态决定提示, 杀进程或上报.

二, 掉帧与帧预算

16.7 ms 只是 60 Hz 下一帧周期的近似示例, 不是所有设备的固定标准:

刷新率单帧周期近似值
60 Hz16.7 ms
90 Hz11.1 ms
120 Hz8.3 ms

实际是否掉帧还受 VSYNC, 输入, 主线程, RenderThread, GPU, SurfaceFlinger 和可变刷新率影响. 应使用 frame timeline/jank 证据, 而不是看到某方法超过 16.7 ms 就直接下结论.

三, 标准排查路径

  1. 先分类: ANR, 主线程长任务, 锁等待, Binder 阻塞, I/O, GC, GPU/渲染还是系统资源不足.
  2. 收集 ANR traces, ApplicationExitInfo, logcat, Perfetto 和 Android vitals 聚合数据.
  3. 对齐时间线: 主线程状态, 锁 owner, Binder 对端, CPU 调度, I/O 和内存压力.
  4. 修复后验证功能正确性, 帧时间分布和线上 ANR rate, 而不是只验证 “不再弹框”.

四, 常见根因

  • 主线程同步网络, 磁盘 I/O, 数据库大查询或大对象序列化.
  • 主线程等待锁/Future.get(), 锁 owner 又被低优先级线程或 Binder 对端阻塞.
  • BroadcastReceiver/Service 中执行过长任务, 没有转交合适的调度机制.
  • 大量布局, 重组, 图片解码, GC 或 GPU overdraw 造成连续 jank.
  • 进程处于严重 CPU/内存/thermal 压力, 正常工作也错过 deadline.

五, 线上治理

  • 使用 Android vitals 观察 user-perceived ANR 和版本趋势.
  • 用 ApplicationExitInfo 补充近期退出原因与 traces (受系统和版本支持约束).
  • 按版本, 机型, ABI, 页面, 前后台状态和实验组聚合.
  • 采集调用栈和 trace 时遵守隐私, 采样与存储预算, 并归档匹配的 mapping/native symbols.

实操: 逐段阅读 traces 与最小帧采样

以下是伪造 traces 片段, 仅用于教学, 不是某次真实 ANR:

"main" tid=1 WAITING
  at java.lang.Object.wait(Native Method)
  - waiting on <0x123> (a java.lang.Object)
  - locked <0x456> (a com.example.UiLock)
"worker" tid=31
  at android.os.BinderProxy.transactNative(Native Method)
  - locked <0x123> (a java.lang.Object)

示意 CPU usage 段, 非真实 ANR:

CPU usage from 0ms to 5120ms later:
  32% 1842/com.example.app: 24% user + 8% kernel
  20% 621/surfaceflinger: 9% user + 11% kernel
  71% TOTAL: 52% user + 18% kernel + 1% iowait

先确认发生时间和进程, 再读 main 的状态与等待对象; 这里 Object.wait() 与 WAITING 一致, main 等 <0x123>, owner worker 又停在同步 Binder. 进程汇总 CPU 不能直接归因给该 worker: 即使 app 进程 CPU 高, 也可能来自同进程其他线程. 下一步必须先核对 tid=31 的线程栈, 调度状态与采样证据; 若 TOTAL 接近满载而 app 低 CPU, 再看调度竞争; iowait 高则继续找磁盘路径, 不能只改锁.

真实 traces 常见的 Binder 对端线索形态可能是 BinderProxy.transactNative, nativePollOnce, binder thread 或 outgoing transaction, 并不保证直接打印 “对端进程名”.用 transaction ID, 时间戳, 服务名 (若 trace 可得) 和 Perfetto 的 binder/sched 轨道关联发起方与服务端; 若服务端线程又在锁或 I/O, 才形成 “main 等 worker, worker 等 Binder 对端” 的下一步判断. 修复可能是缩小锁范围, 禁止持锁 Binder, 给工作设置超时; 验证要重放触发路径, 检查 main 不再等待, 对端完成, 并观察线上 ANR rate.

最小采样上下文片段 (API 24+ Activity): FrameMetrics 统计的是窗口帧指标, 适合轻量趋势, 不替代 Perfetto 归因.

val listener = Window.OnFrameMetricsAvailableListener { _, metrics, _ ->
    val totalNs = metrics.getMetric(FrameMetrics.TOTAL_DURATION)
    if (totalNs > 16_700_000L) recordSlowFrame(totalNs) // 60Hz 近似阈值示例
}
window.addOnFrameMetricsAvailableListener(listener, Handler(Looper.getMainLooper()))
// onDestroy: window.removeOnFrameMetricsAvailableListener(listener)

必须保存 listener 并在 onDestroy 调用 window.removeOnFrameMetricsAvailableListener(listener). 代码中的 16_700_000L 仅是 60Hz 的粗筛示例, 不是通用慢帧阈值; Choreographer 相邻 frameTimeNanos 也只反映回调节奏, 需按刷新率, Frame Timeline 和用户可见 jank 分析.

高频面试题

Q1: ANR 和卡顿有什么区别?
卡顿是帧或交互未按预期 deadline 完成; ANR 是系统针对特定组件 / 交互超时做出的诊断结果. 短暂掉帧不一定 ANR, ANR 前也可能经历较长的不可响应.

Q2: 如何分析一个 ANR traces?
先看主线程状态和栈, 再找锁, Binder, I/O 或 CPU 调度证据; 如果主线程在等待锁, 要继续追 owner 线程, 不能停在表面栈帧.

易错点 / 追问

  • 不把所有 ANR 都写成固定 5 秒.
  • 不把 16.7 ms 当所有刷新率和所有阶段的硬阈值.
  • 不用 “全部放 IO 线程” 替代线程模型与取消设计.
  • 线上聚合和本地 Perfetto 各自解决不同问题, 需要关联使用.

版本与参考资料