ANR触发机制:系统如何判断ANR

今天我们来聊聊一个比较硬核的话题——系统到底是怎么判定ANR的。很多同学遇到ANR第一反应就是看logcat,查堆栈,但很少去思考“系统为什么在这个时间点判定它是个ANR”。把这个搞清楚了,你排查问题的思路会清晰很多。

ANR触发的三个必要条件

说白了,ANR不是随随便便就弹出来的。系统判定ANR需要同时满足三个条件:

  1. 主线程在超时时间内没有处理完某个关键消息(比如输入事件、广播、服务启动等)
  2. 对应的“超时闹钟”已经响起——系统在派发任务时就设了一个定时器
  3. 系统没有收到任何“我还在活着”的信号(比如InputDispatcher的“已送达”确认)

我记得刚入行时以为只要主线程卡住就会立刻弹ANR。其实不是。我在项目中遇到过一个极端情况:主线程做了一次10秒的复杂计算,但恰好那段时间没有用户交互事件,结果ANR并没触发。嗯,因为输入事件超时那个定时器根本没启动。

注意:ANR的超时是从“系统派发关键消息到主线程”那一刻开始计时的,不是从“主线程开始干活”开始算。如果主线程自己跑个死循环但没有新事件派发,系统不会主动触发ANR——当然,这种情况很罕见。

各类型ANR的超时时长

不同类型的ANR,超时阈值是不一样的。我把常见类型整理成了一个表格,方便你对照排查:

ANR类型 用户感知 典型超时时长 核心组件 我的实际场景举例
输入事件ANR 触摸/按键无响应 5秒 InputManagerService 我在做一个购物App时被启动页的欢迎动画坑过一次。动画用了一个Handler.postDelayed循环,每次延迟50ms更新UI,但主线程Layout太重,动画帧被卡住,Input事件5秒没处理,直接弹窗。
广播ANR 无直接感知,后台缓慢 前台广播10秒
后台广播60秒
ActivityManagerService 有一次我在后台广播中调用了Room数据库的同步查询,数据量大概20万条,直接干了30多秒。后台广播超时虽然长,但依然超了——60秒是最坏情况,别忘了还有排队时间。
服务ANR 服务启动/绑定无反应 前台服务20秒
后台服务200秒
ActivityManagerService 记着一个文件上传服务,在onStartCommand里直接做了网络请求。上传一个200MB的文件,当时WiFi信号又差,20秒超时直接ANR。其实应该扔到IntentService或WorkManager里去。
ContentProvider ANR App启动过程中卡死 10秒 ActivityManagerService 极少遇到,但一旦遇到就是启动过程的坑。我记得有一个App共用了20多个ContentProvider,每个都做了初始化操作,叠加超时直接崩了。

关键点:前台和后台的超时时长差异很大。Android从8.0开始对后台执行做了严格限制,后台广播和服务的超时时间更长,但并不是给你更多时间干活,而是系统认为后台任务本就不该在主线程上执行。你想想看,200秒的服务超时,多么讽刺——它暗示你:如果这个任务需要这么久,你应该放到后台线程去做。

核心原理:AMS与IMS的协同监控

系统对ANR的监控是两个服务协同完成的:ActivityManagerService(AMS)和InputManagerService(IMS)。我习惯用一句话总结:AMS管组件,IMS管输入。

InputManagerService的监控逻辑

IMS核心是InputDispatcher这个线程。我个人觉得它的设计思路非常优雅——用了一个“超时队列”的机制:

  1. 当用户触摸屏幕,InputReader将事件传给InputDispatcher
  2. InputDispatcher在派发事件到目标窗口的主线程消息队列的同时,启动一个5秒的超时定时器
  3. 如果5秒内InputDispatcher收到了窗口的“已完成处理”回调(通过finishInputEvent),则取消定时器
  4. 如果超时定时器响铃,InputDispatcher直接向AMS发送ANR通知

我在做性能优化时发现一个坑:如果主线程Looper在轮询,但没有能力处理Input事件(比如被一个耗时的sync过程卡住),InputDispatcher并不会知道主线程“卡了”,它只知道“没有收到完成回调”。这就是为什么有时候logcat里看不到明显的死锁,但Input ANR依然触发。

一个小技巧:当你在trace中看到主线程状态是Runnable但卡了很久,多半是CPU被其他进程抢占了,或者主线程在等待某个Binder同步调用的返回。这时候查一下Thread.currentThread().getStackTrace(),通常能看到主线程在BinderProxy.transact上挂死。

ActivityManagerService的监控逻辑

AMS的监控方式更“主动”一些。对于广播、服务、ContentProvider这几种组件,AMS在派发任务时就在内部维护了一个“超时Map”:

  • 广播ANR:AMS将广播封装成BroadcastRecord对象,放到mPendingBroadcast中,同时通过Handler发送一个延迟消息BROADCAST_TIMEOUT_MSG。当广播处理完毕,AMS会移除这个延迟消息。如果消息被触发——说明广播没在超时内完成。
  • 服务ANR:服务启动时AMS会记录mStartRequested时间戳。如果到超时时间(20秒/200秒)服务还没进入onStartCommand的后续阶段(比如ServiceRecord.executeNestCount递增),就会触发ANR。
  • ContentProvider ANR:这是最容易被忽略的。当App启动时,如果ContentProvider的onCreate()超过10秒,AMS会弹ANR。我记着在Android 9上一个App因为ContentProvider里做了SharedPreference的大文件加载,10秒超限,导致冷启动必ANR。

你如果看过AMS源码,会发现它维护着一个mAnrTimeouts的ArrayList,里面存着所有正在监控的超时任务。每个任务都是一个AnrRecord对象:

// 框架层伪代码,展示AMS的超时监控思路
class AnrRecord {
    String type;      // "input", "broadcast", "service", "provider"
    long timeoutAt;   // 系统期望的最晚完成时间
    int pid;          // 目标进程
    String reason;    // 超时原因简述
    
    boolean isExpired() {
        return SystemClock.uptimeMillis() > timeoutAt;
    }
}

// AMS内部,每个派发的任务都会在mAnrTimeouts里注册
void dispatchBroadcast(BroadcastRecord r) {
    AnrRecord anr = new AnrRecord();
    anr.type = "broadcast";
    anr.timeoutAt = SystemClock.uptimeMillis() + 10000; // 前台广播10秒
    anr.pid = r.receiverApp.uid;
    mAnrTimeouts.add(anr);
    
    // 当广播处理完成后,AMS会调用removeAnrRecord(anr)
    // 如果超时循环发现anr还在列表里,就会触发ANR
}

系统判断ANR的时序图

下面这张图展示了从用户触摸到ANR触发的完整调用链,我把它画成了一个流程:

ANR触发判断时序流程 用户输入事件 四大组件超时 InputManagerService ActivityManagerService InputDispatcher 超时定时器生效 mAnrTimeouts 检查 触发 ANR 弹窗 ⚠ 核心判断依据 系统不关心“代码是否还在执行”,只关心“超时前主线程是否处理完关键消息” AMS/IMS 通过超时定时器与完成回调的“配对检查”来判定ANR 深入阅读:参考 AOSP 源码的 /frameworks/base/services/core/java/com/android/server/wm/InputMonitor.java 以及 /frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

一个容易被忽略的细节:超时时钟源

系统使用两个不同的时钟源来判断ANR:

  • SystemClock.uptimeMillis():用于AMS和IMS的超时判断,这个时钟在设备休眠时会停止。所以你的手机放在桌上黑屏时,不会触发ANR——因为没有“运行时间”在走。
  • SystemClock.elapsedRealtime():用于记录真实时间,在日志中你可以看到ANR发生的精确时间戳。

实战建议:当你分析traces.txt时,注意看uptime和elapsed两个时间戳的差值。如果差值很大(比如uptime只有10分钟但elapsed已经过了1小时),说明设备中途有深度休眠,你的ANR可能是在唤醒后的瞬间被触发的。这种情况下,主线程堆栈往往指向锁竞争或Binder调用。

最后说一个经验

我曾经踩过一个坑:在子线程中通过runOnUiThread更新UI,但主线程当时在做SharedPreferences的commit()操作——这个操作是同步写入磁盘的。当子线程大量调用runOnUiThread时,主线程的消息队列积压了成百上千个Runnable,系统在5秒内无法处理新的Input事件,最终触发ANR。你以为子线程不会卡主线程?你想想看,消息队列积压也是一种ANR的隐形杀手。

嗯,关于触发机制的核心内容就这些。记住这张流程和超时表格,排查ANR时你会知道从哪里开始看,看哪里,看多久。