Android ANR 问题详解

1. 核心结论:什么是 ANR?

ANR(Application Not Responding)是 Android 系统保护用户体验的最后防线。 它的本质是系统跨进程的超时监控:系统核心进程(SystemServer)发现应用主线程未能在规定时间内响应特定事件,从而强制弹窗让用户干预(等待或关闭应用)。

核心解决思路:将耗时操作移出主线程,并优化主线程的锁与资源争用。

2. 为什么需要 ANR 机制?

如果应用在主线程卡死,用户将无法进行任何交互,手机看起来像“死机”了一样。

3. ANR 的触发场景与阈值

发生 ANR 的场景严格限定在以下四类(分别对应不同的系统服务监控):

场景触发阈值监控负责人
Input 触摸/按键5 秒InputDispatcher (WMS 体系)
BroadcastReceiver前台 10 秒 / 后台 60 秒ActivityManagerService (AMS)
Service前台 20 秒 / 后台 200 秒ActivityManagerService (AMS)
ContentProvider10 秒ActivityManagerService (AMS)

4. 核心原理:ANR 是如何监控并触发的?

Android 实现 ANR 的核心思想是**“跨进程监控”**:监控方在系统进程,被监控方在应用进程。主要分为两套模型:

4.1 AMS 的“定时炸弹”模型(针对 Service/Broadcast)

这是最典型的组件超时监控模型。

  1. 埋下炸弹:AMS 准备调度应用执行 Service 的 onCreate 时,先在 AMS 自己的系统线程里发送一个延时 20 秒的消息(埋炸弹)。
  2. 点燃导火索:AMS 通过 Binder 调用应用进程,让应用主线程去执行 Service 代码。
  3. 拆除炸弹:如果应用主线程按时执行完,会通过 Binder 反向通知 AMS。AMS 收到后移除延时消息(拆除炸弹)。
  4. 炸弹爆炸:如果应用主线程卡死,20 秒后延时消息触发,AMS 判定该应用发生 ANR。

4.2 WMS 的“等待 ACK”模型(针对 Input 事件)

对于触摸和按键事件,采用的是类似网络请求的 ACK 确认机制。

  1. 发送事件:InputDispatcher 将触摸事件分发给当前处于焦点状态的应用窗口。
  2. 等待回执 (ACK):发送后,InputDispatcher 等待应用主线程处理完毕并返回 ACK。
  3. 超时判定:如果应用卡住,迟迟不回 ACK。当下一个输入事件到来时,InputDispatcher 发现上一个事件已经等了超过 5 秒,就会直接触发 Input ANR。(注意:如果没有后续输入事件,单次卡顿未必立刻触发 ANR)

4.3 为什么需要两套截然不同的模型?

之所以不统一用一种模型,是因为触发源头和发生频率完全不同,系统为了极致的性能权衡做出了区分:

  1. 组件生命周期(低频、系统驱动):Service/Broadcast 的启动是由系统(AMS)主动发起的,频率极低。系统知道自己什么时候下发了任务,所以可以从容地“埋炸弹”,并等待应用的“拆炸弹”回执。这种主动定时的开销微乎其微。
  2. 输入事件(高频、用户驱动):用户的滑动屏幕会产生密集的 Touch 事件(每秒 60~120 次)。如果 InputDispatcher 给每一个极高频的触摸事件都去单独设一个 5 秒的定时器(埋炸弹),不仅会引发海量的计时任务,还会白白消耗大量 CPU 和内存。 因此,输入系统采用了**“懒惰(Lazy)判定”的 ACK 模型:只在发送事件时记录时间戳,且只有当下一个新事件**到来时,才去顺便检查一下上一个事件是不是等了太久(>5秒)。这种方式巧妙地避开了高频定时器的性能灾难。

4.4 信息收集与弹窗流程

当系统判定 ANR 发生后,会执行以下流程保留“案发现场”:

  1. 发信号:系统向目标应用进程发送 SIGQUIT (kill -3) 信号。
  2. 抓堆栈:应用进程中专用的 SignalCatcher 守护线程收到信号,暂停应用所有线程,抓取当前调用栈并写入 /data/anr/traces.txt
  3. 弹 UI:系统弹出 ANR 提示框,询问用户是等待还是关闭。

5. 常见误区与排查思路

5.1 两个常见误区

5.2 排查思路

排查 ANR 核心是看 /data/anr/traces.txt 文件,重点观察主线程的状态

  1. Runnable(就绪/运行态):可能是死循环,也可能是 CPU 饥饿(线程在就绪队列等分配时间片,但系统负载太高得不到调度)。
  2. Blocked(阻塞态):主线程在等一把锁。需要顺着日志往下找,看是哪个子线程持有了这把锁,并分析该子线程为何迟迟不释放。
  3. Waiting(等待态):等待条件变量唤醒。
  4. Native:主线程在执行 Native 代码,常见于 Binder 同步调用对端进程卡住。

注:线上环境无法直接拉取 traces 文件,高版本可通过 ApplicationExitInfo API 获取退出原因,或接入 Matrix 等 APM 工具通过拦截 SIGQUIT 信号主动抓取堆栈。


6. 面试精简总结(口述版)