ANR复现策略与日志定位指南 - 第3节
一、ANR 到底是怎么被触发的?
很多人以为 ANR 是系统突然弹个框,其实背后有一套严谨的监控机制。说白了,就是系统给你设了个定时炸弹,超时不拆就炸。
我刚开始看这部分源码时,一直有个疑问:系统怎么知道主线程卡了?后来才发现,其实它根本不知道你卡不卡,它只关心你的定时器有没有在时间内被取消。嗯,这里要先理解几个关键的超时阈值。
1.1 四大 ANR 触发场景和超时时间
| 场景 | 超时时间 | 触发条件(从发送消息开始) |
|---|---|---|
| Input ANR | 5 秒 | 5 秒内未完成 input 事件分发 |
| Service ANR | 20 秒 | onStartCommand / onCreate 未在 20s 内返回 |
| Broadcast ANR | 10 秒 | onReceive 未在 10s 内执行完毕(前台) |
| ContentProvider ANR | 10 秒 | onCreate / query 等未在 10s 内返回 |
1.2 触发机制的核心链路
我个人习惯把 ANR 触发抽象成三个角色:定时器(Timer)、执行者(Executor)、观察者(Watcher)。
- 定时器:AMS 内的 Handler 发送一个延迟消息,消息里带着要检查的进程标记。这个延迟时间就是超时阈值。
- 执行者:目标进程的主线程负责处理业务消息。业务消息完成后,会发一个取消定时器的消息回 AMS。
- 观察者:如果超时消息被触发(即取消消息没来),AMS 就认为该进程无响应,开始 dump 堆栈、弹 ANR 对话框。
你想想看,这个流程是不是很像倒计时炸弹?拆弹成功就不炸,拆弹失败就炸。我曾在一个项目里遇到过,因为锁竞争导致取消消息延迟了几十毫秒,结果就炸了 ANR…… 嗯,那是个惨痛的教训。
1.3 关键源码类与流程
拿 Service ANR 举例,核心入口在 ActiveServices.realStartServiceLocked() 里:
// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
void realStartServiceLocked(ServiceRecord r, ...) {
// 1. 发送延迟消息:20秒后超时
mAm.mHandler.sendMessageDelayed(
mAm.mHandler.obtainMessage(SERVICE_TIMEOUT_MSG, r),
mAm.mConstants.mServiceTimeout); // 默认20s
// 2. 真正启动服务(通过Binder调进程)
r.app.thread.scheduleCreateService(r, ...);
// 3. 服务创建完成后,进程回调
// AMS 会在 handleCreateService 中移除超时消息
}
SERVICE_TIMEOUT_MSG,它由 AMS 的主 Handler 处理。一旦处理,就会走 serviceTimeout() -> appNotResponding()。
appNotResponding() 这个方法在 ProcessRecord 里,它会做几件事:
- 收集主线程和各个线程的 native 栈(通过
dumpStackTraces()) - 写入 traces.txt 文件(默认路径
/data/anr/traces.txt) - 弹出 ANR 对话框(如果进程不是后台进程)
- 如果进程持续 ANR 次数过多,最终会 kill 掉进程
1.4 为什么 Input ANR 只有 5 秒?
这个问题在面试里经常被问。我说一下自己的理解:Input 事件直接关系到用户操作的响应感。5 秒已经是容忍上限了,超过这个时间用户就会觉得“手机死了”。所以它用了最短的超时时间。而且 Input ANR 的监控不在 AMS 里,而是在 InputDispatcher 里。
InputDispatcher 每次分发事件时,会设置一个 ANR_TIMEOUT_DURATION 的 timeout。如果目标窗口的输入通道在 5s 内没返回“处理完成”的信号,InputDispatcher 就直接向 AMS 发一个 notifyANR()。这个路径比 Service ANR 更直接。
1.5 不太容易注意到的 ContentProvider ANR
我曾经在一个旧项目里遇到过,某个 ContentProvider 的 query() 里做了耗时数据库迁移,结果一调用就 ANR。当时查了很久才明白:ContentProvider 的 ANR 监控是跟着 ProviderMap 走的,超时时间固定 10s,而且只有在其他进程通过 Binder 调用它的时候才会触发 —— 如果是同一个进程自己调用,反而不会。
ContentResolver.query() 且没有切线程,导致主线程 wait 在 Binder 同步调用上死等。
二、触发流程全景图
下面这张 SVG 图把整个 ANR 触发流程串起来。你看完应该能明白:从用户操作或系统消息开始,到最终弹框,中间经历了几个关键判定点。
2.1 正常流程 vs 异常流程
正常流程里,消息处理完后会发一个 REMOVE_TIMEOUT 消息回来。比如 Service 的 handleCreateService() 里就会调用 mAm.mHandler.removeMessages(SERVICE_TIMEOUT_MSG, r)。这样定时器就被拆除了。异常流程就是定时器被触发了,然后走上面图的右侧路径。
Native methods 或 syscall,那多半是 Binder 调用或者文件 IO 超时了。我在项目里判断这类问题时,会配合 dumpsys activity processes 的输出,看进程的 wait 状态。
三、触发机制中的几个隐藏细节
3.1 超时时间并不是固定的
嗯,要注意 Android 系统有一些可调节参数。比如 ActivityManagerConstants 里就有 serviceTimeout、broadcastTimeout 等成员变量,它们可以通过 settings 或者 system_prop 来修改。某些定制 ROM 厂商会延长超时时间,减少用户看到 ANR 的概率。但这对开发者来说反而是个坑——你在真机上测不出 ANR,上了用户手机就炸。
adb shell setprop debug.am.profile 1 开启更严格的 ANR 监控。
3.2 ANR 的开关
系统有一个 anr_state 的开关,在 ProcessRecord 里有个 mAnrState 字段。正常状态是 ANR_NONE,当进入 ANR 流程后设为 ANR_PENDING,如果进程被杀死则设为 ANR_KILLED。但这个开关主要是让系统避免重复弹框,跟你写代码关系不大。
3.3 为什么有时候 ANR 不弹框?
很多人碰到过:logcat 里明明有 ANR 日志,但手机屏幕上没弹出“应用无响应”的框。这是因为系统判定该 ANR 是“后台 ANR”或“silent ANR”。比如后台 Service 的 ANR 就不会弹框,只会在日志里记录。从 Android 14 开始,后台 ANR 的处理更激进了,可能直接杀掉进程而不做任何提示。所以,如果你看不到弹框,一定要去 logcat 里搜 ANR in 关键字。
四、快速自查清单
当你拿到一个 ANR 问题,可以先对着下面这张表自检一下:
| 现象 | 可能触发原因 | 关键日志位置 |
|---|---|---|
| 点击没反应,5s 后 ANR | 主线程被 UI 更新或 IPC 阻塞 | InputDispatcher 日志 + 主线程堆栈 |
| 启动 Service 后 20s 才 ANR | onCreate 里做了网络请求或磁盘 IO | ServiceRecord 的 timeout 日志 |
| 发广播后 10s ANR | onReceive 里直接调了异步未处理完 | BroadcastRecord 的 timeout 日志 |
| ContentProvider 被调用后 10s ANR | provider 主线程执行了长事务 | ProviderRecord + Binder 调用栈 |
理解触发机制,是复现和定位 ANR 的第一步。我个人觉得,这块最难的不是记住时间,而是理解系统怎么“感知”到主线程卡住了。实际上系统不感知,它只感知定时器。下次你在 appNotResponding 断点里单步的时候,可以注意一下堆栈里那个 Handler 的消息 ID,不同消息对应不同 ANR 类型,这能帮你快速判断这次 ANR 是哪种场景触发的。