二、Android系统ANR类型详解
聊到ANR,我发现很多开发同学第一反应就是“主线程卡了”。其实这么说不全面。ANR的全称是Application Not Responding,但“Not Responding”的原因并不只有主线程阻塞。系统有一套严谨的检测机制,针对的是四种不同的场景。
我个人习惯把这四种类型比作系统的“四道警戒线”。任何一道线被跨过,系统都会毫不留情地弹ANR。下面我结合自己的实战经验,逐一拆解。
1. InputDispatcher 超时
这是最常见,也是最让人头疼的一种。说白了,就是用户触摸了屏幕,或者按了键,结果系统等了好久(通常是5秒)应用都没反应。
系统 InputDispatcher 线程负责将触摸事件分发给应用窗口。应用的主线程(即UI线程)必须通过 InputEventReceiver 快速消费这个事件。从分发开始到接收端处理完毕,超过5秒就触发ANR。
我在项目中遇到过很多次,场景往往是列表加载时滑动卡死。日志里你会看到类似的痕迹:
// logcat 中抓到的典型模式
ANR in com.example.app (com.example.app/.MainActivity)
Reason: Input dispatching timed out (Waiting because no window has focus...)
请注意,这里的关键词是"No window has focus"或"Waiting to send key event"。
- 触发条件:主线程被长时间阻塞(如大文件读取、网络同步请求)。
- 我的建议:所有耗时操作必须交给子线程。如果你用AsyncTask,现在该换成协程或RxJava了。
2. Service 超时
Service超时和前台显示没太大关系。它的规则是:Service的生命周期函数(如onCreate、onStartCommand、onBind、onDestroy)必须在20秒内执行完毕。如果你在Service里做了耗时操作,系统等不了你。
// logcat 日志示例
ANR in com.example.app (com.example.app/.MyService)
Reason: Executing service com.example.app/.MyService
这里要注意一点:服务超时是20秒,比InputDispatcher宽松很多。但恰恰因为宽松,很多人容易忽略。你想想看,一个网络请求如果运气不好,卡在20秒以上也是可能的。这是很多后台下载任务引发ANR的根本原因。
| 类型 | 超时时间 | 发生场景 |
|---|---|---|
| Service | 20秒 (前台Service可延长) | onCreate/onStartCommand/onBind/onDestroy |
| InputDispatcher | 5秒 | 触摸/按键事件分发 |
3. BroadcastReceiver 超时
这个类型在低版本Android上简直是个“坑王”。BroadcastReceiver的超时时间是10秒(静态注册)或较短的动态注册时间。也就是说,你的onReceive方法必须在10秒内返回。
为什么说它是坑?因为很多第三方SDK或者推送服务,会在onReceive里去干活——比如启动一个Service、等待一个锁、或者直接调用ContentProvider做数据库操作。这些都可能超时。
// logcat 日志特征
ANR in com.example.app (com.example.app/.MyReceiver)
Reason: Broadcast of Intent { act=com.example.ACTION }
我记得自己有一次排查一个奇怪的ANR,发现是某个友商推送的广播处理里做了网络请求。那哥们儿为了减少延迟,没开线程,直接在onReceive里发Http请求。结果就是频繁出现10秒超时。我的建议很简单——广播接收器里只做发通知、唤醒Service这些轻活,真正的工作交给IntentService或JobScheduler。
- 静态广播:onReceive在主线程执行,10秒超时。
- 动态广播:同样在主线程,超时时间视情况而定,但都较短。
4. ContentProvider 超时
这个相对少见,但一旦触发,往往是你数据库访问出了大问题。ContentProvider的超时时间没有严格固定,但通常也是10~15秒。当其他进程通过ContentProvider访问数据时,如果Provider所在进程的主线程阻塞,系统会判定ANR。
说白了,ContentProvider的ANR本质上是跨进程调用超时。你的provider做了一次耗时查询,比如全表扫描或者复杂join,刚好又赶上主线程被其他任务卡住,那结果就是ANR。
// 线索往往在 traces.txt 中
"main" prio=5 tid=1 Native
at android.database.sqlite.SQLiteSession.awaitPreemption(SQLiteSession.java:1770)
- locked <0x06647550> (a android.database.sqlite.SQLiteSession)
这里有个冷知识:如果你自己写ContentProvider给别人用,又没做多线程优化,你就是在给自家应用埋雷。我记得有一次,一个系统应用总是ANR,查了半天发现是ContentProvider的query方法里做了同步的网络数据刷新。嗯,幸好后来改成了异步加载。
"ContentProvider not responding" 的日志,不要慌,去看看 /data/anr/traces.txt 里的主线程堆栈,基本上都是锁竞争或者数据库读写超时。
最后,我整理了一个小表格,方便你快速回忆这四种类型的关键区别。
| ANR类型 | 超时时间 | 触发场景简述 | 典型logcat关键词 |
|---|---|---|---|
| InputDispatcher | 5秒 | 用户触摸/按键无响应 | Input dispatching timed out |
| Service | 20秒 | Service生命周期函数未返回 | Executing service |
| BroadcastReceiver | 10秒 | onReceiver长时间未返回 | Broadcast of Intent |
| ContentProvider | 10~15秒 | 跨进程数据访问超时 | ContentProvider not responding |
好了,这四种类型就是ANR的根因方向。遇到问题别慌,先看日志里是哪种超时,再结合具体场景排查。这里面的细节,其实就是一个字——“慢”。找到那个慢的操作,基本上就找到了一半的答案。