Android ANR 问题详解
1. 核心结论:什么是 ANR?
ANR(Application Not Responding)是 Android 系统保护用户体验的最后防线。 它的本质是系统跨进程的超时监控:系统核心进程(SystemServer)发现应用主线程未能在规定时间内响应特定事件,从而强制弹窗让用户干预(等待或关闭应用)。
核心解决思路:将耗时操作移出主线程,并优化主线程的锁与资源争用。
2. 为什么需要 ANR 机制?
如果应用在主线程卡死,用户将无法进行任何交互,手机看起来像“死机”了一样。
- 目的:把“关闭卡死应用”的选择权交还给用户,避免劣质应用霸占系统资源。
- 设计权衡(上帝视角监控):系统并没有去监控应用主线程的每一个函数调用耗时(那会带来巨大的性能开销)。系统只对特定的核心组件和输入事件进行超时监控。如果应用在主线程执行了 4 秒的耗时操作,只要没碰到输入事件和组件生命周期,系统是不会管的。
3. ANR 的触发场景与阈值
发生 ANR 的场景严格限定在以下四类(分别对应不同的系统服务监控):
| 场景 | 触发阈值 | 监控负责人 |
|---|---|---|
| Input 触摸/按键 | 5 秒 | InputDispatcher (WMS 体系) |
| BroadcastReceiver | 前台 10 秒 / 后台 60 秒 | ActivityManagerService (AMS) |
| Service | 前台 20 秒 / 后台 200 秒 | ActivityManagerService (AMS) |
| ContentProvider | 10 秒 | ActivityManagerService (AMS) |
4. 核心原理:ANR 是如何监控并触发的?
Android 实现 ANR 的核心思想是**“跨进程监控”**:监控方在系统进程,被监控方在应用进程。主要分为两套模型:
4.1 AMS 的“定时炸弹”模型(针对 Service/Broadcast)
这是最典型的组件超时监控模型。
- 埋下炸弹:AMS 准备调度应用执行 Service 的
onCreate时,先在 AMS 自己的系统线程里发送一个延时 20 秒的消息(埋炸弹)。 - 点燃导火索:AMS 通过 Binder 调用应用进程,让应用主线程去执行 Service 代码。
- 拆除炸弹:如果应用主线程按时执行完,会通过 Binder 反向通知 AMS。AMS 收到后移除延时消息(拆除炸弹)。
- 炸弹爆炸:如果应用主线程卡死,20 秒后延时消息触发,AMS 判定该应用发生 ANR。
4.2 WMS 的“等待 ACK”模型(针对 Input 事件)
对于触摸和按键事件,采用的是类似网络请求的 ACK 确认机制。
- 发送事件:InputDispatcher 将触摸事件分发给当前处于焦点状态的应用窗口。
- 等待回执 (ACK):发送后,InputDispatcher 等待应用主线程处理完毕并返回 ACK。
- 超时判定:如果应用卡住,迟迟不回 ACK。当下一个输入事件到来时,InputDispatcher 发现上一个事件已经等了超过 5 秒,就会直接触发 Input ANR。(注意:如果没有后续输入事件,单次卡顿未必立刻触发 ANR)
4.3 为什么需要两套截然不同的模型?
之所以不统一用一种模型,是因为触发源头和发生频率完全不同,系统为了极致的性能权衡做出了区分:
- 组件生命周期(低频、系统驱动):Service/Broadcast 的启动是由系统(AMS)主动发起的,频率极低。系统知道自己什么时候下发了任务,所以可以从容地“埋炸弹”,并等待应用的“拆炸弹”回执。这种主动定时的开销微乎其微。
- 输入事件(高频、用户驱动):用户的滑动屏幕会产生密集的 Touch 事件(每秒 60~120 次)。如果 InputDispatcher 给每一个极高频的触摸事件都去单独设一个 5 秒的定时器(埋炸弹),不仅会引发海量的计时任务,还会白白消耗大量 CPU 和内存。 因此,输入系统采用了**“懒惰(Lazy)判定”的 ACK 模型:只在发送事件时记录时间戳,且只有当下一个新事件**到来时,才去顺便检查一下上一个事件是不是等了太久(>5秒)。这种方式巧妙地避开了高频定时器的性能灾难。
4.4 信息收集与弹窗流程
当系统判定 ANR 发生后,会执行以下流程保留“案发现场”:
- 发信号:系统向目标应用进程发送
SIGQUIT(kill -3) 信号。 - 抓堆栈:应用进程中专用的
SignalCatcher守护线程收到信号,暂停应用所有线程,抓取当前调用栈并写入/data/anr/traces.txt。 - 弹 UI:系统弹出 ANR 提示框,询问用户是等待还是关闭。
5. 常见误区与排查思路
5.1 两个常见误区
- ❌ 误区:ANR 都是因为主线程执行了大量耗时代码(如死循环)。
- ✅ 真相:很多时候主线程并没有执行耗时代码,而是被锁阻塞(等锁),或者由于系统整体 CPU 负载过高导致主线程无法获取时间片(CPU 饥饿),甚至是 Binder 通信对端卡死导致同步调用超时。
- ❌ 误区:只要主线程卡顿超过 5 秒就会触发 ANR。
- ✅ 真相:必须是上述四大场景碰到了阈值。如果没有任何输入事件,也没有组件被系统调度,主线程休眠再久也不会引发系统的 ANR 机制。
5.2 排查思路
排查 ANR 核心是看 /data/anr/traces.txt 文件,重点观察主线程的状态:
- Runnable(就绪/运行态):可能是死循环,也可能是 CPU 饥饿(线程在就绪队列等分配时间片,但系统负载太高得不到调度)。
- Blocked(阻塞态):主线程在等一把锁。需要顺着日志往下找,看是哪个子线程持有了这把锁,并分析该子线程为何迟迟不释放。
- Waiting(等待态):等待条件变量唤醒。
- Native:主线程在执行 Native 代码,常见于 Binder 同步调用对端进程卡住。
注:线上环境无法直接拉取 traces 文件,高版本可通过 ApplicationExitInfo API 获取退出原因,或接入 Matrix 等 APM 工具通过拦截 SIGQUIT 信号主动抓取堆栈。
6. 面试精简总结(口述版)
- 机制原理:AMS 的“定时炸弹”延时消息模型(四大组件) + WMS 的“等待 ACK”超时模型(输入事件)。
- 根本原因:主线程被阻塞。分为应用自身原因(主线程 IO、死锁、等锁)和系统原因(CPU 饥饿、频繁 GC、系统服务卡死)。
- 分析手段:依靠
traces.txt看主线程状态(Runnable / Blocked / Native),结合锁的持有关系和 CPU 使用率综合排查。