4. ANR 的触发阈值与 traces.txt 解读
这一章我们来聊聊 ANR 到底是怎么触发的。说白了,就是系统盯着你的 App,「超时了没?超时了没?」。一旦超时,二话不说就 dump 一份 traces.txt 出来。我个人习惯,拿到 traces 的第一眼不是看堆栈,而是先确认触发类型和超时时间——这两者往往能直接框定排查方向。
4.1 不同 Android 版本的超时时间
ANR 的超时阈值,其实每个版本大同小异,但细节上有些调整。我当年在适配 Android 11 的时候,就踩过 Service 超时的坑。下面这张表是我自己整理常用类型对应的超时值,基本覆盖主流版本。
| ANR 类型 | Android 4.0 ~ 9 | Android 10+ | Android 12+(特殊说明) |
|---|---|---|---|
| Input 事件分发 | 5 秒 | 5 秒 | 5 秒(增加 CPU 降级检测) |
| BroadcastReceiver | 10 秒 | 10 秒 | 10 秒(后台限频时可能更长) |
| Service | 20 秒 | 20 秒(前台 20 秒,后台 200 秒) | 20 秒 / 200 秒 |
| ContentProvider | 10 秒 | 10 秒 | 10 秒 |
| JobScheduler / 异步 | 无 | 30 秒(部分版本) | 30 秒 |
4.2 关键日志:traces.txt 解读
traces.txt 是 ANR 现场的「尸检报告」。你想想看,几百行的 dump,到底看哪几行?我一般先扫这几个字段:
- "main" 线程状态 — 如果看到
BLOCKED或WAITING,基本就是卡住了。 - 堆栈顶部 — 主线程最后在调用哪个方法?比如
Thread.sleep、Binder.transact、Looper.loop? - 锁等待信息 —
--- waiting to lock <...>后面跟着的held by thread X,这就是死锁或者锁竞争的铁证。 - CPU 使用率 — 文件头部有一段
CPU usage数据,看你的进程吃了多少 CPU。如果 CPU 不高但卡,大概率是锁竞争或 I/O。 - 其他线程的状态 — 看看有没有工作线程死循环或也在等待。
随便贴一份我修复过的 traces 片段(简化版):
"main" prio=5 tid=1 BLOCKED
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:136)
- waiting to lock <0x1234abcd> (a java.lang.Object) held by thread 12
...
"Binder:1234_5" prio=5 tid=12 NATIVE
at com.example.api.syncData(Native Method)
- locked <0x1234abcd> (a java.lang.Object)
...
你看,主线程(tid=1)在等待一把锁,而锁被 tid=12 持有。tid=12 在做 syncData 同步调用,这通常是网络 I/O。说白了就是 Binder 线程拿着锁不放,主线程一直等。我在项目里遇到过好几次这种类似死锁——原因是跨进程接口没做超时保护。
tid 编号,别忽略。同一个线程名可能对应不同 tid,但 held by thread X 直接告诉你锁的持有者线程 ID。顺着这个 ID 去找对应堆栈,往往一击必杀。
4.3 阈值差异带来的实际影响
不同版本的阈值变化,最让我头疼的是 Service 后台超时从 20 秒变成 200 秒。你以为 App 没触发 ANR 就安全了?不,系统会默默把你的 Service 标记为「可杀」,然后下一次用户操作发现功能没响应——这比 ANR 还难查。
另外 Android 12 对 Input 超时做了 CPU 降级检测。什么意思呢?就是如果系统发现当前 CPU 很忙,它会先等一会儿再判定 ANR。嗯,这点很聪明,但也导致一些原本 5 秒必现的 ANR 在低端机上变成 10 秒才报——测试时要尤其注意跑在满负载环境下。
1. 设备 Android 版本 → 确定超时阈值是否符合预期
2. ANR 类型(Input / Broadcast / Service / Provider)→ 判断主战场
3. traces.txt 头部
----- pid xxx at yyyy-MM-dd HH:mm:ss ----- 行 → 确认时间戳,对一下 logcat 里当时有没有系统广播或前台切换
我个人习惯在监控平台上把这些字段提取出来,做成面板。一旦 ANR 上报,自动打出「版本 + 类型 + 超时值」三元组,直接筛掉 40% 的无效报警。
4.4 一个真实案例
去年适配 Android 12 时,测试机频繁报 Service ANR。我拿到 traces 后,先看超时类型:"Broadcast of Intent { act=xxx }" 超时 10 秒,但日志头部显示 CPU usage: 8% total。这就奇怪了,CPU 这么低,为什么广播超时?接着看主线程堆栈:
"main" prio=5 tid=1 WAITING
at java.lang.Object.wait(Native Method)
at com.example.MyService.onStartCommand(MyService.java:42)
...
原来 Service 的 onStartCommand 里调用了 wait(),等一个网络回调。而那个回调在 Binder 线程里执行,但 Binder 线程被同一个 Service 的 ContentProvider 查询卡住了。绕来绕去,最终是因为 ContentProvider 在另一个进程里没响应,导致整个死锁链。
解决问题的思路很简单:给 ContentProvider 加超时,同时把 Service 里的 wait() 换成带超时的版本。嗯,从此再没复现。
• 阈值差异一定要结合设备版本解读,别拿旧知识套新系统
• traces.txt 先看头部 CPU,再看主线程状态,最后盯锁等待
• 死锁链里「你等我我等它」的情况,用
waiting to lock + held by thread 串联还原
我自己每次排查 ANR,都会把 traces 里的 waiting to lock 行高亮标出来,然后顺藤摸瓜。这个方法用了五六年,几乎没失手过。