ANR 的类型与分类归纳总结
先聊聊我自己的体会。刚做动态交付那几年,每天被各种 ANR 追着跑,但坦白讲——大部分人都掉进同一个坑里:不分类型,一把抓。你想想看,输入事件超时和 ContentProvider 超时,根本是两码事。我个人习惯是先把 ANR 按“触发源头”分清楚,再谈治理。就像修车,你得先知道是发动机还是变速箱出了问题,对吧?
ANR 的五大“元凶”类型
根据官方定义和实际项目经验,我把 ANR 归纳成五类。每一类的触发条件、典型场景和治理抓手都完全不同。
| 类型 | 触发条件 | 典型触发场景 | 关键特征 |
|---|---|---|---|
| 输入事件 ANR | 主线程 5 秒内未处理完输入事件(KeyEvent / MotionEvent) | 列表滑动卡顿 + 密集网络请求;点击按钮后触发复杂计算 | 用户操作最敏感,几乎必弹框 |
| 广播 ANR | onReceiver 方法 10 秒内未返回(前台)或 60 秒(后台) | 广播中做 DB 写操作、同步网络请求 | 后台广播容易被系统“记仇” |
| 服务 ANR | Service 的 onCreate / onStartCommand / onBind 等 20 秒内未完成 | 启动大量服务、服务中用锁导致死锁 | 多进程场景下更容易出现 |
| ContentProvider ANR | Provider 的超时时间短——常见 10 秒(不同厂商有差异) | 跨进程查询大表、Provider 内部做文件 IO | 容易被测试漏掉,线上出现多 |
| JobScheduler / WorkManager 类超时 | 工单任务超时(默认 10 分钟,但实际计算到主线程被阻塞就可能触发) | 后台任务中直接跑 UI 操作或死循环 | Android 8+ 开始明显增加 |
一个小经验: 我排查线上 ANR 时,会先看 traces.txt 里主线程栈顶函数。如果栈顶是 InputDispatcher,基本是输入事件超时;如果是 ActivityThread.handleReceiver,那就是广播的锅。定位准了,治理才有效率。
从系统视角重新分类:主动 vs 被动
上面是按“触发时机”分的。但站在系统内核角度,还可以分成两类:
- 主动 ANR:主线程确实在忙,没空处理输入/广播/服务。比如在做 Gson 解析、加载大图、执行 SQL。
- 被动 ANR:主线程并不是真的忙,而是被其他线程或系统调用卡住了。比如死锁、Binder 调用被远端阻塞、
CountDownLatch.await()等太久。
我曾经在一个动态交付 Module 里遇到过被动 ANR:主线程调了 Binder.transact() 去获取资源文件,结果远端服务刚好在写文件,握了一把 FileLock。两边互相等,直接 5 秒超时。你看了 traces 会发现主线程根本没干活,就是在等。这种必须抓锁死锁链。
一张图看懂分类体系
下面这张图,我把所有常见 ANR 按“主线程执行路径”和“阻塞源”两个维度做了归类。平时排查时,你可以照着它快速定位战场。
一个容易被漏掉的分类维度:前台 vs 后台
Android 系统对不同进程组的“忍耐度”差很多。前台进程超时 5~10 秒就弹框,后台进程可能等 60 秒甚至更久才弹,或者干脆不弹但悄悄 kill 掉。我们做动态交付经常遇到这种情况:用户杀掉 App 后,后台单独跑个 Service,等 40 秒没响应,系统直接杀掉进程 —— 用户反馈“我的 App 怎么突然没了”,其实背后就是一次无弹框 ANR。
关键数据: 在我负责的动态交付 SDK 中,线上收集的 ANR 里 约 42% 属于后台无弹框型。如果只看前台弹框数据,会漏掉将近一半的治理工作。
实战:如何快速判定一个 ANR 的类型?
- 查 traces.txt 主线程栈。 栈顶函数如果是
android.app.ActivityThread.handleReceiver→ 广播 ANR;如果是android.os.MessageQueue.nativePollOnce且后续是Looper.loop→ 可能不是主线程堵,而是 Binder 调用等。 - 看堆栈里的锁信息。 找到
waiting to lock <0x...>或held by thread,这是被动阻塞的明确证据。 - 二次确认 CPU 使用率。 如果发生 ANR 的瞬间 CPU 占用飙到 90%+,一般主动型;CPU 空闲很多但主线程卡住,大概率是被动型。
- 查 kernel 日志。 看看有没有
Watchdog或InputDispatcher的 timeout 信息,帮你确认是哪种超时触发器。
避坑提醒: 我曾经被一个“假主动 ANR”坑过。traces 显示主线程在做 BitmapFactory.decodeStream,看起来是主动忙,但实际是远端 Binder 服务返回数据太慢,主线程在 readFully() 处等待 IO。你不看 nativePollOnce 和底层调用链根本发现不了。后来我养成了习惯:不要只看第一层堆栈,深入三层以上。
小结:分类的价值在于指导治理策略
搞清楚了类型,后面章节里每一类治理工具和手段才有的放矢。比如:输入事件超时 → 优先减少主线程工作量,用异步 + 预加载;死锁 → 找锁的顺序和超时机制;Binder 阻塞 → 改用回调或 Oneway IPC。没有分类,你连从哪下手都不知道。