ANR 的四大类型:你真的了解它们吗?

做 Android 性能优化的这些年,我见过太多开发者一碰到 ANR 就慌了神。其实 ANR 并不是一个模糊的概念,系统把它分成了四种明确的类型。搞清楚它们之间的区别,你才能对症下药。

系统检测 ANR 的核心逻辑说白了就是:在规定时间内,你的应用没有完成该做的事。但不同组件,超时阈值和触发条件完全不同。我拿一张图给你看看它们的关系。

ANR 四大类型及触发机制 系统检测 ANR 输入事件超时 超时阈值:5秒 广播超时 前台10s / 后台60s 服务超时 超时阈值:20秒 ContentProvider超时 超时阈值:10秒 典型触发场景 ▪ 输入:主线程耗时绘制/同步锁 ▪ 广播:onReceive 执行网络或DB ▪ 服务:onStartCommand 阻塞超过20s ▪ Provider:query() 耗时DB操作 注:四种类型共享同一个主线程,但检测规则各自独立

1. 输入事件超时 —— 用户最直观的痛

这是用户感知最强的 ANR。你想想看,用户点了个按钮,屏幕卡住不动了,过一会儿弹出「应用无响应」对话框。系统是怎么判断的?只要按键或触摸事件在 5 秒内没有被处理,就会触发。

关键点:输入事件超时只看主线程的 Looper 是否在 5 秒内成功分发并处理了该事件。不论是 onClick 里做了网络请求,还是 onDraw 里做了复杂计算,只要占用了主线程超过 5 秒,就中招。

我个人习惯是把所有可能导致主线程卡顿的操作都放到异步线程去。有一次我在项目里排查一个 ANR,发现是一个自定义 View 的 onMeasure 里循环了几千次字符串拼接。嗯,字符串拼接用 + 在低版本 Android 上会创建大量临时对象,GC 频繁导致主线程卡死。避坑指南:永远别在主线程做循环,哪怕看起来很小的循环。

2. 广播超时 —— 隐形的性能杀手

广播 ANR 我遇到的频率其实比输入事件还高。系统会给广播接收器一个执行时间窗口:前台广播 10 秒,后台广播 60 秒。如果 onReceive() 方法在这段时间内没有返回,ANR 就来了。

注意:从 Android 8.0 开始,系统对隐式广播做了很多限制,但注册了 FLAG_RECEIVER_FOREGROUND 的显式广播依然延用前台超时规则。另外 Context.registerReceiver() 方式的动态注册不受后台执行限制影响,但超时阈值依然存在。

我记得之前帮一个团队排查问题,他们的支付回调广播经常导致 ANR。一看代码,onReceive() 里直接调了 ContentResolver.query() 去做数据库写入。广播接收器运行在主线程,这么干必死。后来我建议他们把耗时操作扔到 IntentService 或者使用 goAsync() 方法。对了,goAsync() 可以延长广播处理时间,但注意要在子线程里调用 PendingResult.finish(),不然系统会一直等。

3. 服务超时 —— 后台任务的陷阱

服务 ANR 的触发条件比较特殊。系统并不是监控你的 Service 一直在运行多久,而是看 onStartCommand() 或 onBind() 等生命周期方法是否在20 秒内返回。说白了,系统不关心你的后台任务跑多久,它只关心你启动服务的那个方法有没有及时响应。

触发方法超时时间常见原因
onStartCommand()20s方法内执行网络请求、数据库操作
onBind()20s远程服务绑定过程出现死锁
onCreate()20s初始化资源过慢(比如加载大图片)

警惕:很多开发者以为把 Service 放到独立进程就可以避免 ANR,其实不是。独立进程只是不让 UI 线程卡顿,但服务本身的超时检测依然存在。我曾经见过一个应用把 Service 放在独立进程,但 onStartCommand 里开了个 Thread.sleep(30000) — 结果 ANR 照出不误,因为主线程(Service 的 main 线程)还是被阻塞了。

避坑指南:onStartCommand() 里面千万不要做任何耗时操作。正确的做法是启动一个 IntentService 或者直接用 HandlerThread,让实际业务在子线程里执行,onStartCommand 立即返回。

4. ContentProvider超时 —— 数据访问的瓶颈

这个类型比较冷门,但一旦发生就很头疼。系统规定:ContentProvider 的 CRUD 操作(query、insert、update、delete)必须在 10 秒内返回。注意,这里的时间是进程间通信(IPC)的整个链路的耗时,包括 Binder 传输时间和 Provider 内部执行时间。

我遇到过一个案例:应用启动时通过 ContentProvider 读取用户配置,那块数据库因为没建索引,十几万行数据的 query 跑了 8 秒,加上跨进程 Binder 拷贝,正好卡在 10 秒边缘。偶尔一次网络抖动 ANR 就爆了。后来加了索引,查询降到 200 毫秒,问题解决。

容易忽略的场景:如果你的应用作为 ContentProvider 提供数据给其他应用调用,那你不仅是给自己挖坑,还可能拖慢别的应用。比如系统通讯录 Provider 被卡住时,整个系统的拨号、短信都会受影响 — 那种 ANR 会被系统判定为系统级问题,甚至可能触发重启。

四大类型对比总结

我把它们整理成一个速查表,方便你调试时一眼定位。

ANR 类型超时阈值主线程阻塞点我遇到最多的场景
输入事件超时5s事件分发、布局、绘制列表滑动时做大量图片加载
广播超时前台10s / 后台60sonReceive() 方法广播接收器内直接做网络请求
服务超时20sonStartCommand() / onBind()服务启动时进行数据初始化
ContentProvider超时10squery/insert/update/delete跨进程大数据量查询无索引

为什么要了解这些阈值?因为你的优化目标就是让主线程的每个任务在对应时限内完成。没有比 5 秒更紧急的,也没有比 60 秒更能容忍的。下一类问题我们会深入输入事件超时,看看那些隐藏在主线程里的「慢性杀手」是怎么悄悄拖垮你的 App 的。