初识 ANR:为什么你的 App 会「卡死」?
干了这么多年 Android 开发,我个人觉得 ANR 是最让人头疼的问题之一。不为什么,因为它直接卡用户的手指。用户正刷着朋友圈呢,突然点不动了,心里肯定一肚子火。嗯,我们今天就来好好聊聊 ANR 到底是什么。
1.1 什么是 ANR?
ANR 全称是 Application Not Responding,也就是「应用无响应」。说白了,就是系统觉得你的 App 太忙了,没空搭理用户的点击操作。
- 官方定义: 当应用的主线程(UI 线程)被阻塞超过一定时间,系统就会弹出一个 ANR 对话框。
- 用户视角: 屏幕卡死,点击没反应,几秒后弹出「XXX 应用无响应」的窗口。
- 本质原因: 主线程上执行了耗时操作,或者发生了死锁,导致无法处理新的 UI 事件。
核心点: ANR 不是 Crash,系统不会直接 Kill 应用,而是会先给用户一个选择——「等待」还是「关闭」。
我记得有一次,一个用户反馈说打开某个页面就黑屏,然后弹出 ANR。我一看 traces 文件,好家伙,主线程正在等待一把锁,这把锁被一个子线程死死攥着不放。那个子线程还在做网络请求,网络一慢,整个 App 就挂了。这就是典型的 ANR 场景。
1.2 ANR 的本质
你想想看,Android 系统为什么能实时响应用户的触摸?因为有一个叫 Looper 的东西在死循环里不停地从 MessageQueue 里取消息。如果主线程取出一条消息后,花了超过 5 秒才处理完,那 Looper 就被卡住了,新的触摸事件也就没办法派发了。
所以 ANR 的本质可以归纳为这样一句话:Android 的消息处理机制被阻塞了。
上面这张图基本上把本章的知识体系串起来了。下面我们一个点一个点地拆开说。
1.3 ANR 的五大类型
说白了,系统并不是什么事儿都管。它只监控以下四种核心场景,再加上一个「其他」。我习惯把它们记作「IBSP」加上一个兜底。下面这个表格一目了然:
| 类型 | 超时时间(前台) | 超时时间(后台) | 监控机制 |
|---|---|---|---|
| InputDispatching Timeout | 5 秒 | 5 秒 | Input 事件 5 秒未处理完毕 |
| BroadcastReceiver Timeout | 10 秒 | 60 秒 | onReceive 未在规定时间返回 |
| Service Timeout | 20 秒 | 200 秒 | onCreate / onStartCommand 超时 |
| ContentProvider Timeout | 10 秒 | 10 秒 | Provider 发布或 query 超时 |
一个小技巧: 后台的 Service 超时时间长达 200 秒,前台只有 20 秒。这意味着你的 App 如果切到后台,Service 可以慢悠悠地工作,但一旦回到前台,立刻就得麻溜起来。我曾经在一个后台下载任务上踩过坑,前台 Service 里做了一个 30 秒的耗时操作,用户回来后直接弹 ANR。
1.4 触发场景详解
理解了类型,我们再看看具体什么情况下会触发。嗯,这里要注意,很多开发者以为只有主线程做耗时操作才会出问题,其实不止。
1.4.1 输入分发超时
这是最经典的 ANR 场景。主线程在处理一个触摸事件或者按键事件时,超过 5 秒没有调用下一个事件分发。说白了,系统等不及了。
- 常见原因: 主线程中做了大量数据库操作、网络请求(虽然网络库一般会自动切线程)、或者主线程被锁死。
- 避坑指南: 我曾经在一个监控 App 里把一个庞大 JSON 解析写在了 Activity 的 onCreate 里,用户一打开页面就卡 6 秒以上,ANR 率飙升。后来果断扔到子线程 + 协程里,问题就解决了。
1.4.2 广播超时
这个场景比较隐蔽。你可能会想,广播接收器的 onReceive 方法不是在主线程运行吗?对,如果在 onReceive 里做耗时操作,直接触发 ANR。
而且广播有前后台之分。前台广播(通过 Context.sendBroadcast 发送)必须在 10 秒内返回,后台广播则是 60 秒。
警告: 千万不要在 onReceive 里直接启动一个线程去做耗时任务,因为 onReceive 返回后,系统认为广播处理已经结束了。如果这时候线程还在运行,一旦发生问题,系统不会认为这是广播的锅——它会在其他场景下触发 ANR,而且日志往往不明显。我曾经就因为这个背过锅,查了三天才发现广播里偷偷开了一个线程在写文件。
1.4.3 服务超时
Service 的超时时间比较长,前台 20 秒。但正因为时间长,开发者反而容易放松警惕。我见过有人把几十 MB 的文件解析放在了 Service 的 onCreate 里,导致第一次启动 Service 时 ANR。
我的个人习惯是,在 Service 里凡是耗时的操作,一律用 IntentService 或者 WorkManager,顺手把超时时间也算进去。
1.4.4 ContentProvider 超时
这个相对少见,但一旦碰上就很棘手。Provider 的超时多发生在两个 App 通过 Provider 共享数据时,比如系统通信录 App。如果你在 query 里做了复杂查询,或者 Provider 首次创建时做了太多初始化工作,就很有可能超时。
1.5 如何获取 ANR 日志?
好了,理论说完了,咱们来点实际的。遇到 ANR 后,第一件事就是找 traces 文件。这玩意儿是 ANR 分析的核心证据。
// 传统方式
adb pull /data/anr/traces.txt
// 或者查看最新的 ANR 日志
adb bugreport
// 在 Android 10 及以上,系统会把 ANR 日志存放在
/data/anr/ 目录下,
文件名通常是 anr_xxxxx_xxxxx。
关键日志段: 打开 traces 文件后,直接搜索 "ANR in" 或者 "DALVIK THREADS"。你会看到类似这样的信息:
----- pid 12345 at 2023-01-01 12:00:00 -----
Cmd line: com.example.yourapp
...
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x... self=0x...
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x...
| state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
| stack=0x... stackSize=8MB
| held by tid=5 (Thread-5)
at com.example.yourapp.MainActivity.onClick(MainActivity.java:25)
- waiting to lock <0x...> (a java.lang.Object)
at com.example.yourapp.OtherThread.doWork(OtherThread.java:10)
- locked <0x...> (a java.lang.Object)
at ...
trace 文件里,重点关注这几个字段:
- "main" 线程的状态: 如果是
Blocked,说明它在等锁;如果是Runnable,说明它在忙 CPU 计算。 - held by: 哪个线程持有了主线程需要的锁?这是死锁分析的关键。
- stack 调用: 找到主线程卡死的具体代码行。
说实话,很多人刚看 traces 文件会觉得眼花缭乱。我的建议是,别急,先找到 "main" 这一行,然后往上往下各看 20 行,基本就能定位到问题函数了。一次不行就多看几次,习惯了就好。
1.6 本章总结
这一章我们主要理清了 ANR 是什么、有哪些类型,以及日志怎么看。说白了,ANR 就是主线程被堵住了。我个人的经验是:先搞清楚类型,再去看 traces 文件,定位代码位置,最后修复。