ANR 与卡顿排查
ANR (Application Not Responding) 和卡顿 (Jank) 是导致用户流失的直接原因。作为中高级开发者,仅仅知道 ANR 的触发时间是不够的,必须具备看懂 traces.txt 和系统调度的能力。
一、ANR 的本质与类型
ANR 的触发条件往往被记忆为几个固定阈值(如输入事件 5s),但底层本质是:主线程无法在系统规定的时间预算内,响应系统派发的特定事件或完成相应的生命周期调度。
常见的 ANR 类型:
- KeyDispatchTimeout:输入事件(点击/触摸)超过 5 秒无响应。
- BroadcastTimeout:前台广播 10 秒,后台广播 60 秒未执行完毕。
- ServiceTimeout:前台 Service 20 秒,后台 Service 200 秒未启动/绑定完成。
- ContentProvider:
publish超时(通常为 10 秒)。
二、ANR / 卡顿的核心诱因
ANR 和卡顿(丢帧)在表现上严重程度不同,但诱因往往相似——主线程被占据。主要原因可分为三大类:
- 主线程做耗时操作:如直接在主线程解析大 JSON、读写 SharedPreferences (尤其是老版本)、或者加载庞大的布局导致 measure/layout 时间过长。
- 锁等待 (Lock Wait) 与死锁:主线程等待工作线程释放某个锁,而工作线程迟迟不释放;或者主线程与工作线程互相等待对方的锁(Deadlock)。
- 系统层面阻塞:
- Binder 阻塞:主线程发起同步 IPC 调用,对端进程卡住或系统负载过高导致超时。
- CPU 饥饿:后台大量高优先级线程抢占 CPU,或者系统整体处于高负载,分配给当前 App 主线程的时间片极少。
三、ANR 与卡顿 (Jank) 的区别联系
- 卡顿 (Jank):主线程并未完全死锁,只是单次处理事件/渲染耗时超过了 16.6ms(60Hz 屏幕),导致丢帧。用户觉得“不跟手”。
- ANR:主线程彻底罢工或执行超长任务(如长达 5 秒甚至更久),完全失去响应能力。
- 联系:持续的严重卡顿,若碰巧阻塞了系统的输入事件或广播调度,就会升级为 ANR。
四、排查路径与流程(本地 vs 线上)
解决此类问题同样依赖不同阶段的针对性工具。
| 环境 | 分析目标 | 工具使用 |
|---|---|---|
| 本地定位 | 发现掉帧原因、死锁确认 | StrictMode、Systrace/Perfetto、Android Studio Profiler |
| 线上监控 | 大盘 ANR 聚类、卡顿率统计 | ANR-WatchDog、线上监控平台、抓取 /data/anr/traces.txt 提取上报 |
ANR 定位流程:
- 获取 Traces 文件:当 ANR 发生时,系统会生成
traces.txt(位于/data/anr/下,高版本可能聚合成了 Bugreport 格式)。 - 分析 Main Thread 堆栈:打开 traces 文件,首先寻找
main线程的调用栈。- 状态是
Runnable?可能是死循环或大计算量代码。 - 状态是
Blocked?说明在等锁,需要顺藤摸瓜找当前占有这把锁的线程(held by owner)。
- 状态是
- 结合 CPU / IO 状态:看 traces 头部,检查当时的 CPU 负载(User/System/IOWait)。如果 IOWait 极高,说明主线程可能阻塞在磁盘操作;如果 CPU 总体负载达到 100%,哪怕主线程是简单的运算也会因分配不到时间片而 ANR。
- 验证 Binder 对端:如果卡在
BinderProxy.transact,需分析当时的系统服务或其他进程状态是否异常。
五、ANR-WatchDog (线上监控思路)
线上由于权限限制,难以直接去 /data/anr/ 拉取完整的 traces。
常见的应用层监控手段是基于 WatchDog 的思路:
- 启动一个后台线程(WatchDog 线程)。
- 每隔固定时间(如 5 秒),向主线程的消息队列
MessageQueue抛一个任务(修改某个标志位)。 - WatchDog 线程休眠 5 秒后醒来,检查标志位是否被修改。如果没有修改,说明主线程 5 秒内都没能处理这个极其简单的消息,说明主线程卡死了。
- 此时由 WatchDog 线程主动去
dump主线程的堆栈信息并上报服务器。
高频面试题
Q1: 看 traces.txt 定位 ANR,主线程处于 Runnable 状态就一定是代码在死循环吗? 不一定。Runnable 只代表在操作系统的就绪队列中,或者正在运行。如果系统 CPU 资源极度紧张被其他进程抢占,主线程虽然是 Runnable 但实际上得不到 CPU 时间片,也会发生 ANR。
Q2: 什么是 Binder 阻塞引起的 ANR?
当主线程调用诸如获取系统服务(如 ActivityManager、PackageManager)的方法时,通常是一个跨进程的同步调用(IPC)。如果当时系统服务端负载过高或卡死,主线程就会一直等对端返回,最终导致 ANR。
Q3: 如何用工具查出是布局层级过深导致的滑动卡顿?
可以使用 Layout Inspector 查看 View 树的层级结构。同时,抓取一段滑动的 Perfetto (或 Systrace) 文件,观察 RenderThread 和 Main Thread 上的 Choreographer#doFrame 切片,看里头的 measure/layout 子片段是否消耗了大量时间(远超 16ms)。
易错点 / 追问
- 误区: “发生 ANR 一定是我的代码写错了。” 很多时候线上的 ANR 是由于手机老化、后台其他应用疯狂抢占资源(CPU/IO)导致的,或者厂商系统的 Bug 导致 Binder 响应缓慢。
- 追问: 如果 traces 看到主线程卡在底层的
epoll_wait或者MessageQueue.nativePollOnce,说明什么?(答:这通常说明主线程是空闲的,在等待新的消息,并没有阻塞。如果此时发生了 ANR,极有可能是 ANR 发生瞬间主线程刚好处理完了阻塞任务进入空闲,或者是系统组件派发消息出现了时序错乱。) - 易错点: 将卡顿监控的阈值设得太低。线上网络和设备环境复杂,过于激进的卡顿阈值会产生海量无效上报,淹没真正的性能瓶颈。