Android ANR 全解析与 traces 分析实战
技术复盘 · 面试复习手册
四类超时判定 · 系统级 Watchdog · 三段式栈分析
目录
- 一、先厘清概念:ANR 到底是谁判定的
- 二、四类 ANR 与超时阈值
- 三、还有一类"系统级 ANR":Watchdog
- 四、按根因分类:比背类型更有价值
- 五、traces 文件怎么读:三段式方法
- 六、三个典型实战案例
- 七、定位工具箱
- 八、预防:应用侧与系统侧
- 九、面试速答卡
摘要(TL;DR)
ANR 不是"卡了 5 秒"这么简单:它是 AMS 对四类超时(输入 5 秒、广播 10/60 秒、Service 20/200 秒、Provider 10 秒)的判定结果,而 traces 文件里的 main 线程栈顶、锁持有者和 CPU usage 段,才是定位根因的三把钥匙。
一、先厘清概念:ANR 到底是谁判定的
ANR(Application Not Responding)的本质是系统对应用响应能力的一次超时判定,判定方不是用户、也不是应用自己,而是运行在system_server中的 AMS 以及 InputDispatcher。
判定发生后,系统会做三件事:
- 把应用所有线程的堆栈 dump 到
/data/anr/目录(Android 11 起文件名形如anr_2026-05-20-10-30-00,早期统一写traces.txt); - 在 logcat 中打印
ANR in 包名与Reason:; - 弹出"应用无响应"对话框(部分场景如后台 Service、Provider 超时,不弹框直接记日志)。
注意
只有主线程被卡住才会 ANR。后台线程再慢也不会直接触发 ANR,但它可能占满 CPU 或持有主线程需要的锁,从而间接导致 ANR。
二、四类 ANR 与超时阈值
| 类型 | 触发条件 | 阈值 | 日志特征 | 判定位置 |
|---|---|---|---|---|
| 输入事件分发超时 | InputDispatcher 已把按键/触摸事件发给窗口,应用 5 秒内未消费 | 5 s | Reason: Input dispatching timed out | InputDispatcher 上报 AMS |
| 广播超时 | 广播接收者的onReceive()未在时限内返回 | 前台 10 s / 后台 60 s | Broadcast of Intent { ... } | BroadcastQueue |
| Service 超时 | onCreate/onStartCommand/onBind等生命周期回调未完成 | 前台 20 s / 后台 200 s | executing service 包名/服务名 | ActiveServices |
| ContentProvider 超时 | Provider 进程拉起或 publishProvider 未完成 | 10 s | ContentProvider not responding | ProcessList(进程启动超时) |
记忆口诀:输入 5、广播 10/60、服务 20/200、Provider 10。
几个容易答错的细节:
- 服务端超时口径在 Android 8 之后固定为前台 20 秒、后台 200 秒,不要再背老版本的"后台 20 分钟";
- 广播的"前台/后台"取决于发送方是否处于前台,不是接收方;
- Android 12 起前台服务启动限制更严,但 ANR 阈值未变。
三、还有一类"系统级 ANR":Watchdog
面试官如果问"你处理过系统死机吗",答的就是这一层。
system_server内部有一个 Watchdog 线程,默认 60 秒为一轮,向若干被监控者(AMS 的 main handler、PMS 的 handler、WMS 的全局锁等)投放检测消息。若 60 秒内没有回执,Watchdog 会:
- dump 所有 Java 线程栈与 native 栈到
/data/anr/; - 打印
*** WATCHDOG KILLING SYSTEM PROCESS ***; - 主动杀死
system_server,zygote 感知后重启框架(部分平台直接整机重启)。
所以"手机突然黑屏重启、看不到应用崩溃"的现场,第一件事就是找WATCHDOG KILLING这个关键字。
四、按根因分类:比背类型更有价值
| 根因 | 典型栈特征 | 常见场景 |
|---|---|---|
| 锁等待 | Blocked,栈中有waiting to lock ... held by thread N | 主线程与子线程共用锁;子线程做耗时 IO 时持锁 |
| 主线程 IO | 栈顶在SharedPreferencesImpl、SQLiteConnection、FileInputStream | onCreate里读配置、写日志 |
| Binder 同步阻塞 | 栈顶在BinderProxy.transactNative | 主线程调系统服务或其它进程的 AIDL 同步接口 |
| 消息队列堆积 | 栈顶在MessageQueue.next之下的业务代码,且 utm 很高 | 大量 Handler 消息串行执行,输入事件排在队尾 |
| CPU 饥饿 | 主线程栈正常(甚至nativePollOnce),但 CPU usage 段显示被占满 | 后台线程死循环、其它进程抢占 |
| GC 抖动 | 栈中Daemon/HeapTaskDaemon活跃,utm 偏高 | 内存抖动频繁触发 GC |
| 系统服务端阻塞 | 主线程卡在ActivityManagerProxy/PackageManagerProxy | PMS 安装扫描、WMS 布局耗时、system_server 锁竞争 |
五、traces 文件怎么读:三段式方法
读 traces 的关键不是"看得多",而是"看得准"。下面按三段固定顺序拆解,每一段只回答一个问题。
一份 traces 文件包含多个进程的完整栈,几万行起步。不要从头读,按下面顺序看三段。
5.1 第一段:现象行与 Reason
----- pid 4213 at 2026-05-20 10:30:12 ----- Cmd line: com.example.app Build fingerprint: 'xxx/release-keys' ABI: 'arm64'往上翻日志里的ANR in com.example.app,重点看Reason:与PID:,先把"哪一类 ANR、卡在哪个进程"确定下来。
5.2 第二段:main 线程栈(核心中的核心)
"main" prio=5 tid=1 Blocked | group="main" sCount=1 dsCount=0 flags=1 obj=0x12c00000 self=0x7f8a1c0000 | sysTid=4213 nice=-10 cgrp=default sched=0/0 handle=0x7f9b2c4a50 | state=S schedstat=( 1200000000 8500000000 4200 ) utm=95 stm=25 core=3 HZ=100 | stack=0x7fe3a00000 stackSize=8192KB | held mutexes= at com.example.core.LockHelper.refresh(LockHelper.java:42) - waiting to lock <0x0a1b2c3d> (a java.lang.Object) held by thread 27 at com.example.MainActivity.onResume(MainActivity.java:88) at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1456)这份栈要读出四个信息:
- 线程状态
Blocked:不是Running,说明主线程在等锁; waiting to lock ... held by thread 27:锁被 27 号线程持有,马上去找Thread-27的栈;utm=95 stm=25:主线程累计用户态 95 个时钟 tick,说明它不是刚被唤醒,而是已经干了不少活;schedstat的第二项 8.5 秒:可运行但没抢到 CPU 的时间,这个值大就高度怀疑 CPU 饥饿。
常见的良性栈形(别误判):
at android.os.MessageQueue.nativePollOnce(Native method)且状态为Native/Waiting:主线程空闲等待消息,正常;at java.lang.Object.wait且是Waiting:等待通知,需结合业务判断。
5.3 第三段:CPU usage 段
CPU usage from 0ms to 5000ms later: 78% 4213/com.example.app: 70% user + 8% kernel / faults: 1200 minor 52% 4213/com.example.app (main thread) 18% 4218/OkHttp Dispatcher 12% 812/media.codec: 12% user解读规则:
- 主线程自身 CPU 高:主线程在死循环、密集计算或频繁 GC;
- 主线程 CPU 低但其它线程/进程高:CPU 饥饿型 ANR,主线程抢不到时间片;
- 总和接近 100%:系统整体过载,需要看是否有第三方进程或系统服务异常。
5.4 补充:Binder 阻塞怎么确认
如果 main 栈顶是下面的样子,说明主线程在等system_server或其它进程返回:
at android.os.BinderProxy.transactNative(Native method) at android.app.IActivityManager$Stub$Proxy.startService(...)此时要找 traces 中对应进程(通常是system_server)的栈,看它卡在哪把锁上——这才是"跨进程死锁"的真实位置。
六、三个典型实战案例
下面三个现场覆盖了八成以上的 ANR 投诉:锁、跨进程、CPU。每个案例只保留关键栈片段,训练"从栈到结论"的直觉。
案例 1:主线程等锁(最高频)
现象:点击首页按钮后 5 秒弹 ANR。
关键栈:
"main" prio=5 tid=1 Blocked at com.example.db.ConfigDao.get(ConfigDao.java:31) - waiting to lock <0x0a1b2c3d> held by thread 27 "Thread-27" prio=5 tid=27 Runnable at com.example.net.HttpClient.query(HttpClient.java:120) - locked <0x0a1b2c3d> (a java.lang.Object)结论:子线程 27 持锁做网络请求,主线程被迫等待。
修复:锁粒度收窄,把网络请求移出临界区;主线程改为异步回调。
案例 2:Binder 同步调用引发的跨进程死锁
现象:开机向导进入设置页必现 ANR,且system_server也出现卡顿。
应用侧:
"main" ... Native at android.os.BinderProxy.transactNative(Native method) at android.content.pm.IPackageManager$Stub$Proxy.getPackageInfo(...)system_server侧:
"Binder:1234_5" ... Blocked at com.android.server.wm.ActivityTaskManagerService.startActivity(...) - waiting to lock <0x0b2c> held by "main" "main" ... Blocked at com.android.server.wm.WindowManagerService.layoutWindow(...) - waiting to lock <0x0c3d> held by "Binder:1234_5"结论:应用主线程同步等 PMS,PMS 又在等 WMS 的锁,而 WMS 的锁被另一个正在等应用的 Binder 线程持有,形成环。
修复:应用侧改异步查询;系统侧减少持锁时间,避免在锁内做跨进程调用。
案例 3:CPU 饥饿型(最容易被误判为"代码写得烂")
现象:低端机上偶发 ANR,栈看起来完全正常。
关键特征:
- main 栈顶为
nativePollOnce(空闲); schedstat第二项达数秒;- CPU usage 段显示某后台进程占 60% 以上。
结论:不是应用代码卡住,而是系统整体 CPU 被占满,输入事件分发不出去。
修复:定位抢占进程(多为异常后台服务、日志抓取、频繁 GC 的第三方 SDK),必要时用 cgroup 限流或调低优先级。
七、定位工具箱
# 1. 抓取现网 ANR 文件(需 root 或 userdebug)adb shellls-l/data/anr/ adb pull /data/anr/anr_2026-05-20-10-30-00# 2. 事件日志中的 ANR 埋点adb logcat-bevents|grep-E"am_anr|am_proc_start"# 3. 实时观察输入分发是否卡住adb shell dumpsys input|grep-A20"FocusedWindow"# 4. 查看当前进程与线程 CPU 占用adb shelltop-H-p进程号# 5. 系统服务锁竞争(userdebug)adb shell dumpsys activity processes adb shell dumpsys window离线分析顺序建议:Reason→ main 栈 → 锁持有者 → 对端进程栈 → CPU usage。按这个顺序,八成现场能在十分钟内定性。
八、预防:应用侧与系统侧
ANR 治理要分两侧看:应用侧决定"会不会卡",系统侧决定"卡了之后能不能快速恢复"。
应用侧(写代码时就能规避)
- 主线程不做 IO、不碰数据库、不读大文件;
onReceive()中超过 10 秒的工作改用goAsync()或转 Service/WorkManager;- 避免主线程同步调用 Binder,改用回调或线程池;
- 减少
synchronized范围,尤其不要"持锁做网络/IO"; - 开启 StrictMode,在开发期暴露主线程磁盘与网络访问;
- Application 的
onCreate只做初始化,重活延迟到首屏后。
系统侧(ROM/平台工程师的职责)
- 缩短
system_server关键锁的持有时间,避免锁内跨进程调用; - 监控开机后前 3 分钟的 CPU 峰值,限制预装应用的并发启动;
- 对高频 ANR 的服务做专项治理(如 PMS 扫描、WMS 布局);
- 在 userdebug 版本常开 Watchdog 日志,量产版本保留触发时的 dump 能力。
九、面试速答卡
问:ANR 有哪几种?
答:表面四类超时——输入 5 秒、广播前台 10 秒后台 60 秒、Service 前台 20 秒后台 200 秒、Provider 10 秒;本质都是主线程 5 秒内无法处理输入事件。再往上一层还有system_server的 Watchdog,60 秒无响应会杀系统进程,属于整机级的"系统 ANR"。
问:怎么定位 ANR?
答:先看Reason定类型,再看 traces 里 main 线程的栈顶与状态,然后顺着held by thread N找到锁持有者,必要时对比对端进程(Binder 对端)的栈,最后用 CPU usage 段区分"自己在忙"还是"CPU 被抢"。四步能覆盖绝大多数现场。
问:你做过哪些 ANR 治理?
答:分两层讲——应用层收敛主线程 IO 与锁粒度,系统层治理system_server锁竞争与开机 CPU 峰值;配合 traces 自动化解析脚本,把高频 ANR 聚合成 TOP 榜,按模块派单。
元信息
| 项目 | 说明 |
|---|---|
| 目标读者 | Android 系统开发工程师、Framework 层开发者、面试冲刺者 |
| 前置知识 | Android 进程模型、Binder 机制、AMS 基本职责、Linux 线程状态 |
| 技术栈版本 | Android 8.0 至 Android 14(阈值与路径以 AOSP 主线为准) |