二、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了。
避坑指南:我曾经见过一个案例,某应用在页面跳转时做了复杂的布局解析,明明就几毫秒的事,但因为连续跳转又叠加了同步操作,累积超过了5秒。所以,瞬时卡顿不一定会触发ANR,但连续卡顿一定会。

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秒 触摸/按键事件分发
请牢记:很多人以为Service运行在后台就可以为所欲为。这是一个致命的误解。Service虽然不在前台,但它的主线程依然是UI线程。如果你直接在里面做数据库升级、大文件写操作,自然会触发20秒的Timeout。

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方法里做了同步的网络数据刷新。嗯,幸好后来改成了异步加载。

排查技巧:如果你在logcat里看到类似 "ContentProvider not responding" 的日志,不要慌,去看看 /data/anr/traces.txt 里的主线程堆栈,基本上都是锁竞争或者数据库读写超时。
Android ANR 类型与超时体系 Android 系统 ANR InputDispatcher 超时:5秒 Service 超时:20秒 BroadcastReceiver 超时:10秒 ContentProvider 超时:10~15秒 关键日志特征 1. Input: "Input dispatching timed out" 2. Service: "Executing service ..." 3. Broadcast: "Broadcast of Intent ..." 4. ContentProvider: "ContentProvider not responding" 常见根因定位点 1. 主线程耗时操作(IO/网络) 2. 数据库锁竞争(SQLite锁) 3. View布局嵌套过深导致inflate慢 4. Binder调用卡死(死锁) 时间规范:不同组件超时阈值不同,定位方法一致

最后,我整理了一个小表格,方便你快速回忆这四种类型的关键区别。

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的根因方向。遇到问题别慌,先看日志里是哪种超时,再结合具体场景排查。这里面的细节,其实就是一个字——“慢”。找到那个慢的操作,基本上就找到了一半的答案。