Appearance
Android ANR 触发机制、排障定位与防范
回到总览:并发与运行时底座
相关模块:Android 主线程消息循环:Looper / Handler / MessageQueue · Android Binder IPC 机制与跨进程通信
一句话定义
ANR(Application Not Responding,应用无响应)是 Android 系统 AMS 检测到主线程在规定时间内未能响应特定事件(输入事件、Service 生命周期、BroadcastReceiver、ContentProvider)时,通过信号弹出的系统级无响应机制。
代码索引
为什么需要
- 为什么主线程耗时一定会引发 ANR?
- 一句话答:Android 的 UI 渲染与四大组件回调均在主线程消息队列(
MessageQueue)中排队;当某个消息阻塞主线程超过系统阀值时,后续的输入事件或组件生命周期消息无法被及时处理,系统定时器就会被触发打断并抛出 ANR。
- 一句话答:Android 的 UI 渲染与四大组件回调均在主线程消息队列(
- 为什么 ANR 发生在主线程,但根因却经常在子线程?
- 一句话答:主线程可能在等待子线程持有的锁(Lock Contention),或者子线程死循环做 CPU 密集计算占满了多核资源(CPU Starvation),导致主线程即使空闲也分不到 CPU 时间片执行。
- 在 Flutter / 混合开发中如何防范 ANR?
- 一句话答:Flutter 在 Android 平台上的
UI TaskRunner默认运行在宿主主线程上;如果 Dart 代码中触发了密集型同步计算或 PlatformChannel 同步调用阻塞了主线程,同样会导致 Android 宿主抛出 ANR。
- 一句话答:Flutter 在 Android 平台上的
底层机制
1. ANR 的四大触发场景与超时阈值
对应 Lab:anr-mechanism-demo
Android 系统在 AMS(ActivityManagerService)中为四大场景分别设置了倒计时定时器(Handler 延迟消息):
| ANR 类型 | 触发条件 | 前台阈值 | 后台阈值 | 触发点 |
|---|---|---|---|---|
| InputDispatching | 按键或触摸事件 5s 内未处理完成 | 5 秒 | 5 秒 | InputDispatcher |
| Service | onCreate() / onStartCommand() 超时 | 20 秒 | 200 秒 | AMS.ServiceTimeout |
| BroadcastReceiver | onReceive() 执行超时 | 10 秒 | 60 秒 | BroadcastQueue |
| ContentProvider | publish() 或 query() 启动超时 | 20 秒 | 20 秒 | AMS.ProviderTimeout |
2. ANR 的系统倒计时工作原理
以 Service 启动为例,AMS 的倒计时过程如下:
图注补充:Handler: 2. postDelayed 发送 20s 超时 ANR 消息;AMS: 3. Service 启动完成,发送 serviceDone 回执;Handler: 4. 移除/取消 20s 延迟 ANR 消息;AMS: 5. 20s 延迟消息到期触发, 执行 appNotResponding;App: 6. 发送 SIGQUIT(3), 收集 traces.txt 并弹出 ANR 框。
- AMS 在发起
startService请求的同时,向自身的AnrHandler发送一个 20 秒延迟的 ANR 消息。 - 如果 App 主线程在 20 秒内完成了 Service 的
onCreate(),会向 AMS 发送回执,AMS 收到回执后取消/移除该延迟 ANR 消息。 - 如果主线程卡死(如发生死锁、主线程 I/O),AMS 20 秒内未收到回执,延迟消息被触发,AMS 倾倒堆栈信息(Dump Stacks)并弹出 ANR 对话框。
3. ANR 发生的常见三大根因类型
- 主线程直接阻塞:主线程做同步网络请求、大文件 I/O、复杂的数据库查询。
- 主线程锁等待 (Lock Contention):主线程尝试获取某个对象的
synchronized锁,而该锁被子线程持有,且子线程正在执行耗时 I/O。 - CPU 资源耗尽 (CPU Starvation):后台多个子线程在死循环做密集计算,抢占了 CPU 时间片,导致主线程即使没有阻塞也无法分到 CPU 资源执行。
Android / Flutter / Web / Backend 对照
| 维度 | Android 原生 | Flutter / Dart | Web / Backend 对照 |
|---|---|---|---|
| 卡死机制 | 系统级 ANR (AMS 定时器 + 弹框) | 页面假死 / 触发宿主 ANR | 浏览器页面未响应 (Unresponsive Page) |
| 检测手段 | BlockCanary / Perfetto / traces.txt | Performance Overlay / DevTools Profiler | Long Tasks API / Sentry |
| 异步避坑 | 异步子线程 / Coroutines Dispatchers.IO | compute() / Isolate.spawn() 搬走重活 | Web Worker 移走耗时计算 |
常见场景
1. 主线程尝试获取子线程持有的 synchronized 锁
java
// 耗时子线程
new Thread(() -> {
synchronized (mLock) {
// 子线程持有锁并做耗时 File I/O
readBigFile();
}
}).start();
// 主线程
public void onClick(View v) {
synchronized (mLock) { // 主线程在 onClick 中挂起等待 mLock,触发 Input ANR!
doSomething();
}
}2. BroadcastReceiver 中做耗时网络操作
在 onReceive() 中直接发网络请求,前台广播超出 10 秒即抛出 Broadcast ANR。
常见误配、事故后果与排障
1. 如何分析 traces.txt 定位 ANR 根因
从 /data/anr/traces.txt 中搜索 main 线程:
text
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 flags=1 obj=0x73243180 self=0x7f8d682000
| sysTid=1234 nice=-10 cgrp=default sched=0/0 handle=0x7f91a50498
| state=S schedstat=( 12345678 87654321 123 ) utm=1 stm=0 core=2 HZ=100
| stack=0x7ffe420000-0x7ffe422000 stackSize=8MB
| held mutexes=
at com.example.MainActivity.onClick(MainActivity.java:45)
- waiting to lock <0x0c3124ab> (a java.lang.Object) held by thread 12 <-- 明确指出被 thread 12 持有!排障步骤:
- 观察
main线程状态:如果为Waiting/Blocked,寻找- waiting to lock <id>。 - 顺藤摸瓜查找
tid=12线程的堆栈,查看该线程在做何事(如java.io.FileInputStream.read)。 - 结论:由于子线程持锁做 File I/O,阻塞了主线程
onClick响应,导致 ANR。
2. 防范策略与工具链
- 开发阶段:开启
StrictMode严格模式,违规主线程 I/O 直接在 Logcat 报错或 Crash。 - 线上监控:接入
BlockCanary(基于 HandlerPrinter监听dispatchMessage的前后时间差),若单条消息耗时超过 200ms 即打印卡顿调用栈。
与相近概念对比
| 概念 | 表现形式 | 是否崩溃 (Crash) | 解决手段 |
|---|---|---|---|
| ANR | 系统弹框“应用无响应”,用户可选择关掉或等待 | 否 (系统主动杀进程) | 耗时逻辑移出主线程、解除死锁 |
| Crash (OOM/Exception) | 进程直接闪退解体 | 是 | 捕获异常、优化内存 |
| Jank (卡顿/掉帧) | 画面微卡、丢帧,但未达到 ANR 秒级阈值 | 否 | 优化布局嵌套、减少主线程轻微耗时 |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| anr-mechanism-demo | 主线程阻塞 6 秒触发 ANR + adb 诊断命令 | AnrMechanismDemoActivity.kt |
复习检查题
BroadcastReceiver 的
onReceive()默认运行在哪条线程?超时时间是多少?答:默认运行在 App 的主线程。前台广播的超时限制为 10 秒,后台广播为 60 秒。如果在
onReceive()中做耗时操作,需开启GoAsync()或交由WorkManager处理。为什么 CPU Starvation (CPU 被占满) 也会导致没有任何锁阻塞的主线程触发 ANR?
答:因为 Android 是多任务操作系统,当后台多个子线程在死循环或密集计算占满了所有 CPU 核心(CPU 使用率 100%)时,操作系统内核给主线程分配的 CPU 时间片急剧减少,导致主线程即便消息队列正常,也无法获得足够的 CPU 执行周期在规定时间内处理完输入事件,从而触发 ANR。
速记
- 四场景阈值:Input 5s,Broadcast 10s,Service 20s,ContentProvider 20s。
- ANR 三根因:主线程直接耗时/IO、主线程锁等待 (Lock Contention)、CPU 满载抢占 (CPU Starvation)。
- 排障口诀:先查
main线程状态,Blocked 找持锁 thread,Waiting 找挂起点,Runnable 查 CPU 负载。