3. 常见ANR场景分析

聊到ANR,很多人第一反应是「主线程卡了」。但如果你仔细看过ANR日志,会发现系统把它分了好几类:输入事件超时、广播超时、服务超时、ContentProvider超时。这几种场景的触发条件和排查思路完全不同。

我个人习惯先把它们拆开看,而不是一股脑扫日志。下面逐个讲。

3.1 输入事件ANR

这是最常见、用户感知最直接的一类。你点一下屏幕,5秒没反应,系统就弹「应用无响应」了。

关键词: 5秒阈值 · 主线程 · 触摸事件 · 输入调度器

触发条件其实很单纯:InputDispatcher 把事件分发给应用,应用主线程在5秒内没处理完。为什么没处理完?说白了就是主线程被其他活儿占着。

我遇到过最坑的一次:一个列表页的点击事件,里面居然串行做了数据库读写,而且数据量上去之后,一次查询就要2秒。用户多点两下,ANR就稳了。

常见根因:

  • 主线程在做网络请求或文件IO
  • 锁等待——子线程占着锁不放,主线程也在等同一把锁
  • View绘制太重——比如复杂动画或者超大Bitmap
  • Binder调用卡住——例如Service双向通信死锁
避坑指南: 我曾经在onClick里直接调了 Thread.sleep() ——没错,当时为了调试延迟效果,上线前忘了删。5秒一到,ANR日志里清清楚楚看到主线程卡在sleep上。所以我的建议是:所有可疑的延迟操作,先在代码里搜一遍 Thread.sleep()、Looper.getMainLooper() 上的耗时调用。

3.2 广播ANR

广播ANR分两种:静态注册广播(manifest)和动态注册广播。前者超时阈值是10秒,后者是动态的,但系统一般也按10秒来算。为什么广播也会ANR?因为广播接收器默认跑在主线程。

注意: 即使你在广播里开了子线程做耗时操作,如果广播onReceive方法本身没返回,系统仍然认为它在执行。很多开发者在这里犯错:开了线程,但没调用 goAsync(),然后onReceive直接返回了——系统以为处理完了,实际后台线程还在跑,不会ANR。但要注意生命周期。

具体来说:

  • 静态广播:耗时操作必须用 goAsync() 并持有一个 BroadcastReceiver.PendingResult,超过10秒没调用 finish() 就算超时。
  • 动态广播:同上,但前台广播超时时间是10秒,后台广播在某些版本会更短。

我记得有一次线上反馈:用户手机在低电量时经常弹ANR。排查后发现是一个系统广播 ACTION_BATTERY_CHANGED 被多个应用同时接收,我们的广播接收器里做了SharedPreferences写入——虽然写文件不慢,但并发起来就卡主线程了。

3.3 服务ANR

服务ANR的场景稍微隐蔽一点。系统规定:前台服务在20秒内没执行完 onCreate()、onStartCommand() 或 onBind() 就算超时。后台服务的超时时间更长(200秒),但一般不会等到那么久用户就杀应用了。

服务ANR的核心矛盾是:服务本身是方便后台任务的,但它的生命周期回调却是在主线程。你如果在 onStartCommand() 里直接做网络请求,20秒内没返回,ANR就来了。

解决方案常规套路:

  1. 耗时代码放到 IntentService 或 JobIntentService 里
  2. 用 HandlerThread 自己起工作线程
  3. 用 LiveData 或 RxJava 异步回调
一个真实案例: 我接手过一个即时通讯App,它的长连接心跳是在Service的onStartCommand里直接调阻塞socket。网络差的时候,socket连接3秒才超时,但重试逻辑写成了while(true)循环,直接卡死主线程。最终定位到问题,改成异步连接就好了。

3.4 ContentProvider ANR

ContentProvider的ANR相对少见,但一旦出现,往往和锁或者数据库相关。触发条件:当ContentProvider的 query()、insert()、update()、delete() 方法在主线程执行时间超过系统阈值(同样是5~10秒级别)时就会ANR。

但有个细节:ContentProvider的CRUD方法默认跑在调用方进程的Binder线程池中,而不是ContentProvider所在进程的主线程。那为什么还会ANR?因为如果ContentProvider的实现里用了锁,而锁又和主线程有交互——比如你在query里等一个主线程发出的回调——就可能死锁或超时。

经验之谈: 我排查过一个ContentProvider ANR:第三方SDK的ContentProvider在 query() 里调了 getContext() 获取Application,而Application初始化时又去调这个ContentProvider,形成了循环依赖。最终导致Binder线程等待Application初始化,Application等待ContentProvider返回——互相等死。

另外,ContentProvider的 openFile() 方法如果直接在主线程做了大文件读写,也很容易触发ANR。我记得有一款相册App,通过ContentProvider返回图片InputStream,内部却在Binder线程里做了BitmapFactory.decodeStream——大于2MB的图直接卡了。


3.5 四种ANR场景对比

下面这个表格是我自己在做预案时整理的,你拿去直接参考:

ANR类型超时阈值执行线程常见根因
输入事件5秒主线程UI耗时、锁等待、Binder阻塞
广播(静态)10秒主线程onReceive里做IO、忘了goAsync
服务(前台)20秒主线程回调onStartCommand做网络/DB
ContentProvider5~10秒Binder线程(但锁可能导致主线程等待)数据库锁、循环依赖、大文件读写

你如果把这四种场景的阈值和线程归属记清楚,再看ANR日志里的CPU usage和main thread stack,基本能秒定位。

3.6 一张图理清ANR场景

下面我手画了一张关系图,帮你把上面讲的串起来。

常见ANR场景分类与触发关系 常见ANR场景分类 输入事件ANR 主线程、5秒 广播ANR 主线程、10秒 服务ANR 主线程、20秒 ContentProvider ANR Binder线程、5~10秒 UI耗时 / 锁等待 主线程忙 / Binder阻塞 View绘制超时 文件IO / 网络IO onReceive耗时 忘了goAsync() DB / SP写入 多广播并发 onStartCommand耗时 直接做网络请求 阻塞socket / 死循环 未启用子线程 数据库锁 循环依赖 大文件读写 Binder线程回等主线程 核心排查思路:看主线程堆栈 + 看锁等待 + 看CPU使用率 日志关键字:"main thread" "waiting to lock" "CPU usage" 以上四种ANR场景,本质都是主线程或关键线程被阻塞超过阈值

3.7 小结

这四种场景,每种都有自己的「脾气」。输入事件ANR最直接,用户立刻感知;广播ANR常常发生在系统事件密集的时候;服务ANR容易被忽略,因为开发者总觉得服务是后台;ContentProvider ANR则多跟锁和跨进程有关。你只要把阈值记住,再配合日志里的堆栈,基本半小时内能定位根因。

一句话总结: ANR不是玄学,是主线程超时。谁占用了主线程,谁就是凶手。