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 内返回
很多人会问:后台广播是不是不同?是的,后台广播超时是 60 秒。但注意,从 Android 8.0 开始后台广播限制很严,实际触发 ANR 的概率反而低了。

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 中移除超时消息
}
关键点:超时消息的 ID 是 SERVICE_TIMEOUT_MSG,它由 AMS 的主 Handler 处理。一旦处理,就会走 serviceTimeout() -> appNotResponding()。

appNotResponding() 这个方法在 ProcessRecord 里,它会做几件事:

  1. 收集主线程和各个线程的 native 栈(通过 dumpStackTraces())
  2. 写入 traces.txt 文件(默认路径 /data/anr/traces.txt)
  3. 弹出 ANR 对话框(如果进程不是后台进程)
  4. 如果进程持续 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 调用它的时候才会触发 —— 如果是同一个进程自己调用,反而不会。

注意坑:很多第三方 SDK 在你的进程里直接 new ContentProvider 的方式去查数据,如果那个 Provider 在主进程,且你主线程调用了耗时操作,很容易触发 ContentProvider ANR。我曾经被某个广告 SDK 坑过一次,它内部用了 ContentResolver.query() 且没有切线程,导致主线程 wait 在 Binder 同步调用上死等。

二、触发流程全景图

下面这张 SVG 图把整个 ANR 触发流程串起来。你看完应该能明白:从用户操作或系统消息开始,到最终弹框,中间经历了几个关键判定点。

Android ANR 触发流程 用户操作 / 系统消息 系统服务设置超时 Handler Input 5s Service 20s / Broadcast 10s InputDispatcher 等待 AMS Handler 消息 超时消息触发 dumpStackTraces() 弹出 ANR 对话框

2.1 正常流程 vs 异常流程

正常流程里,消息处理完后会发一个 REMOVE_TIMEOUT 消息回来。比如 Service 的 handleCreateService() 里就会调用 mAm.mHandler.removeMessages(SERVICE_TIMEOUT_MSG, r)。这样定时器就被拆除了。异常流程就是定时器被触发了,然后走上面图的右侧路径。

一个小技巧:看 ANR 日志时,先看 traces.txt 里主线程的堆栈顶部,通常就是卡住的地方。如果你的应用在 ANR 日志里频繁出现 Native methods 或 syscall,那多半是 Binder 调用或者文件 IO 超时了。我在项目里判断这类问题时,会配合 dumpsys activity processes 的输出,看进程的 wait 状态。

三、触发机制中的几个隐藏细节

3.1 超时时间并不是固定的

嗯,要注意 Android 系统有一些可调节参数。比如 ActivityManagerConstants 里就有 serviceTimeout、broadcastTimeout 等成员变量,它们可以通过 settings 或者 system_prop 来修改。某些定制 ROM 厂商会延长超时时间,减少用户看到 ANR 的概率。但这对开发者来说反而是个坑——你在真机上测不出 ANR,上了用户手机就炸。

避坑指南:我曾经在小米某款机型上测试,怎么都复现不了 ANR,后来发现它把前台广播超时改成了 30 秒。所以强烈建议:用原生系统或模拟器做 ANR 测试,别太相信定制 ROM 的表现。或者通过 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 是哪种场景触发的。