2. ANR的类型:四种超时的解剖
ANR其实就四种类型。听起来不多,但每一种背后的逻辑完全不同。我刚开始做性能优化时,就吃过“类型搞混”的亏。你想想看,用查输入超时的思路去排查广播超时,那效率能高吗?
今天我们就把这四种类型拆开来看。原因、触发场景、日志特征,一次说透。
2.1 输入超时(Input Dispatch Timeout)
这是最常见的ANR类型。说白了,就是用户点了一下屏幕,等了5秒都没有反应。
系统会给每个输入事件设定一个5秒的“耐心时间”。如果主线程在这5秒内没有处理完上一个事件,新的KeyEvent或MotionEvent就会触发ANR。
关键日志特征:
- 日志开头:
ANR in com.example.app (com.example.app/.MainActivity) - 原因字段:
Input dispatching timed out (Waiting because no window has focus...) - 必有调用栈:以
InputMethodManager或ViewRootImpl结尾
在我的项目中,有一次线上出现大量输入超时,排查发现是某个页面的onCreate里做了加密数据库的初始化。那操作耗时3秒多,后面再叠加列表渲染,就直接超了5秒的线。
排查技巧:
遇到输入超时,80%的锅都在主线程。直接看 main thread 的调用栈。如果是 android.os.MessageQueue.nativePollOnce 卡住,要考虑是不是消息循环被死循环顶住了。
2.2 广播超时(Broadcast Timeout)
广播有两种:前台广播和后台广播。它们的超时时间不一样。
- 前台广播:超时时间默认10秒。需要在
onReceive方法内完成所有工作。 - 后台广播:超时时间默认60秒(Android 8.0之前)。8.0之后后台广播限制加严,很多广播不再直接发送,但超时机制仍然存在。
为什么会这样?因为 onReceive 是跑在主线程上的。你在里面做了数据库写操作,或者请求网络,系统不给你60秒,它就报ANR。
关键日志特征:
- 日志开头:
ANR in com.example.app (com.example.app/.MyBroadcastReceiver) - 原因字段:
Broadcast of Intent { act=CONNECTIVITY_CHANGE flg=0x... } - 必有调用栈:以
BroadcastReceiver$PendingResult或AlarmManager结尾
我曾经遇到过一个非常典型的案例:某第三方SDK在广播接收里做了全量数据同步,网络正常情况下10秒够,但弱网环境下直接超时。解决办法?把耗时操作扔到 IntentService 或 JobScheduler 里。
避坑指南:
我曾经在系统广播中注册了动态监听,忘记了在 onPause 时解除注册。结果页面切到后台后广播还在跑,onReceive 被反复触发,最后酿成连续ANR。记住,动态注册的广播一定要成对使用 registerReceiver 和 unregisterReceiver。
2.3 服务超时(Service Timeout)
服务超时相对隐蔽,因为不是每个App都用大量服务。它的规则是这样的:
- 系统在调用
onCreate或onStartCommand后,会启动20秒倒计时。 - 如果服务在20秒内没有调用
startForeground或主动结束,就会触发ANR。
嗯,这里要注意:Service的ANR不仅仅是主线程的问题,还涉及 Context.startService 和 bindService 的差异。如果是绑定服务,超时时间同样适用,但常见于 onBind 阻塞。
关键日志特征:
- 日志开头:
ANR in com.example.app (com.example.app/.MyService) - 原因字段:
Executing service com.example.app/.MyService - 必有调用栈:以
ActivityThread.handleServiceArgs或Service.onCreate结尾
我记得有一次线上案例,用户反馈在付款后app直接卡死。我通过日志定位发现,原来是一个后台服务在 onStartCommand 里执行了加密密钥生成操作,该操作依赖系统时间戳做随机种子,恰好遇到NTP时间同步导致的死锁。20秒后,ANR弹出。嗯,这种隐晦问题,没有调用栈根本看不出来。
排查技巧:
对于服务超时,建议先取 traces.txt 关注 main thread 是否在等待 Future.get() 或 CountDownLatch.await()。通常问题出在异步转同步的封装上。
2.4 内容提供者超时(ContentProvider Timeout)
内容提供者超时最少见,但一出问题就是大问题。它发生在客户端通过 ContentResolver 访问CP时,系统会设定一个超时阈值(通常是10~15秒)。如果CP的 query、insert、update、delete 方法执行时间过长,就会触发ANR。
为什么会有这种ANR呢?因为 ContentProvider 的 query 方法默认是在Binder线程池中执行的,但如果调用端在主线程直接发起同步查询,那主线程就会一直阻塞等待Binder返回。一旦查询慢,主线程就死在那了。
关键日志特征:
- 日志开头:
ANR in com.example.app (ContentProvider)或ANR in com.example.app (provider) - 原因字段:
ContentProvider not responding - 必有调用栈:以
ContentProviderProxy.query或ContentResolver.query结尾
我遇到过一种特殊情况:某个App同时使用了多个ContentProvider,其中一个Provider的 onCreate 里做了大量初始化,导致其他Provider的客户端在访问时产生Binder超时。嗯,这种其实是“连锁ANR”。
避坑指南:
不要在ContentProvider的 onCreate 中做重操作。系统会在 Application.onCreate 之前就调用它,一旦阻塞,App直接死在启动阶段。我曾经见过一个App的CP onCreate里做了网络请求,结果在弱网下永远起不来,最后还是靠本地缓存才救回来。
四种ANR类型速查表
| 类型 | 触发条件 | 超时值 | 主线程特征 | 日志关键字 |
|---|---|---|---|---|
| 输入超时 | 输入事件5秒未消费 | 5秒 | 主线程在Looper中卡住 | Input dispatching timed out |
| 广播超时 | onReceive超时未返回 | 10秒 / 60秒 | 主线程在onReceive中 | Broadcast of Intent |
| 服务超时 | onCreate/onStartCommand 20秒未完成 | 20秒 | 主线程在Service回调中 | Executing service |
| 内容提供者超时 | CRUD操作超过阈值 | 10~15秒 | 主线程在Binder wait中 | ContentProvider not responding |
搞清楚这四种类型,你拿到ANR日志后就能立刻知道大概方向了。下一块内容,我们重点说说每种类型对应的日志怎么“读”——不是简单看堆栈,而是抓签名模式,一眼判断问题根因。