3. 常见ANR场景分析
聊到ANR,很多人第一反应是「主线程卡了」。但如果你仔细看过ANR日志,会发现系统把它分了好几类:输入事件超时、广播超时、服务超时、ContentProvider超时。这几种场景的触发条件和排查思路完全不同。
我个人习惯先把它们拆开看,而不是一股脑扫日志。下面逐个讲。
3.1 输入事件ANR
这是最常见、用户感知最直接的一类。你点一下屏幕,5秒没反应,系统就弹「应用无响应」了。
触发条件其实很单纯:InputDispatcher 把事件分发给应用,应用主线程在5秒内没处理完。为什么没处理完?说白了就是主线程被其他活儿占着。
我遇到过最坑的一次:一个列表页的点击事件,里面居然串行做了数据库读写,而且数据量上去之后,一次查询就要2秒。用户多点两下,ANR就稳了。
常见根因:
- 主线程在做网络请求或文件IO
- 锁等待——子线程占着锁不放,主线程也在等同一把锁
- View绘制太重——比如复杂动画或者超大Bitmap
- Binder调用卡住——例如Service双向通信死锁
Thread.sleep() ——没错,当时为了调试延迟效果,上线前忘了删。5秒一到,ANR日志里清清楚楚看到主线程卡在sleep上。所以我的建议是:所有可疑的延迟操作,先在代码里搜一遍 Thread.sleep()、Looper.getMainLooper() 上的耗时调用。
3.2 广播ANR
广播ANR分两种:静态注册广播(manifest)和动态注册广播。前者超时阈值是10秒,后者是动态的,但系统一般也按10秒来算。为什么广播也会ANR?因为广播接收器默认跑在主线程。
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就来了。
解决方案常规套路:
- 耗时代码放到
IntentService或JobIntentService里 - 用
HandlerThread自己起工作线程 - 用
LiveData或RxJava异步回调
3.4 ContentProvider ANR
ContentProvider的ANR相对少见,但一旦出现,往往和锁或者数据库相关。触发条件:当ContentProvider的 query()、insert()、update()、delete() 方法在主线程执行时间超过系统阈值(同样是5~10秒级别)时就会ANR。
但有个细节:ContentProvider的CRUD方法默认跑在调用方进程的Binder线程池中,而不是ContentProvider所在进程的主线程。那为什么还会ANR?因为如果ContentProvider的实现里用了锁,而锁又和主线程有交互——比如你在query里等一个主线程发出的回调——就可能死锁或超时。
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 |
| ContentProvider | 5~10秒 | Binder线程(但锁可能导致主线程等待) | 数据库锁、循环依赖、大文件读写 |
你如果把这四种场景的阈值和线程归属记清楚,再看ANR日志里的CPU usage和main thread stack,基本能秒定位。
3.6 一张图理清ANR场景
下面我手画了一张关系图,帮你把上面讲的串起来。
3.7 小结
这四种场景,每种都有自己的「脾气」。输入事件ANR最直接,用户立刻感知;广播ANR常常发生在系统事件密集的时候;服务ANR容易被忽略,因为开发者总觉得服务是后台;ContentProvider ANR则多跟锁和跨进程有关。你只要把阈值记住,再配合日志里的堆栈,基本半小时内能定位根因。