4. ANR 的触发阈值与 traces.txt 解读

这一章我们来聊聊 ANR 到底是怎么触发的。说白了,就是系统盯着你的 App,「超时了没?超时了没?」。一旦超时,二话不说就 dump 一份 traces.txt 出来。我个人习惯,拿到 traces 的第一眼不是看堆栈,而是先确认触发类型和超时时间——这两者往往能直接框定排查方向。

4.1 不同 Android 版本的超时时间

ANR 的超时阈值,其实每个版本大同小异,但细节上有些调整。我当年在适配 Android 11 的时候,就踩过 Service 超时的坑。下面这张表是我自己整理常用类型对应的超时值,基本覆盖主流版本。

ANR 类型Android 4.0 ~ 9Android 10+Android 12+(特殊说明)
Input 事件分发5 秒5 秒5 秒(增加 CPU 降级检测)
BroadcastReceiver10 秒10 秒10 秒(后台限频时可能更长)
Service20 秒20 秒(前台 20 秒,后台 200 秒)20 秒 / 200 秒
ContentProvider10 秒10 秒10 秒
JobScheduler / 异步无30 秒(部分版本)30 秒
⚠️ 注意:Android 10+ 的 Service 后台超时放宽到 200 秒,这可不是给你摸鱼的——而是系统觉得后台服务可以慢慢杀。但如果你在 前台 启动 Service,20 秒内没完成 onStartCommand 就炸。

4.2 关键日志:traces.txt 解读

traces.txt 是 ANR 现场的「尸检报告」。你想想看,几百行的 dump,到底看哪几行?我一般先扫这几个字段:

  1. "main" 线程状态 — 如果看到 BLOCKED 或 WAITING,基本就是卡住了。
  2. 堆栈顶部 — 主线程最后在调用哪个方法?比如 Thread.sleep、Binder.transact、Looper.loop?
  3. 锁等待信息 — --- waiting to lock <...> 后面跟着的 held by thread X,这就是死锁或者锁竞争的铁证。
  4. CPU 使用率 — 文件头部有一段 CPU usage 数据,看你的进程吃了多少 CPU。如果 CPU 不高但卡,大概率是锁竞争或 I/O。
  5. 其他线程的状态 — 看看有没有工作线程死循环或也在等待。

随便贴一份我修复过的 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 线程拿着锁不放,主线程一直等。我在项目里遇到过好几次这种类似死锁——原因是跨进程接口没做超时保护。

💡 经验:traces 里的 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 秒才报——测试时要尤其注意跑在满负载环境下。

🔑 核对清单:当你看到 ANR 日志时,先确认三个东西:
1. 设备 Android 版本 → 确定超时阈值是否符合预期
2. ANR 类型(Input / Broadcast / Service / Provider)→ 判断主战场
3. traces.txt 头部 ----- pid xxx at yyyy-MM-dd HH:mm:ss ----- 行 → 确认时间戳,对一下 logcat 里当时有没有系统广播或前台切换

我个人习惯在监控平台上把这些字段提取出来,做成面板。一旦 ANR 上报,自动打出「版本 + 类型 + 超时值」三元组,直接筛掉 40% 的无效报警。

ANR 触发与 traces.txt 分析流程 ANR 触发 超时判定 生成 traces.txt PID / 时间 / CPU 主线程堆栈 锁等待分析 定位 ANR 根因 经验提示 主线程状态 + 锁等待 是 80% ANR 的突破口 别被 CPU 数据干扰 直接看 waiting to lock

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 行高亮标出来,然后顺藤摸瓜。这个方法用了五六年,几乎没失手过。