3. ANR的类型分类
搞 ANR 监控这么多年,我一直觉得:不懂类型,没法定位。很多人拿到 log 就慌,其实只要看懂了是哪种 ANR,排查方向就清晰一半。今天咱们就把这四种类型掰开揉碎聊聊。
系统把 ANR 分成四大类:输入事件、广播、服务、ContentProvider。每种触发条件、超时时间、监控手段都不一样。我刚开始做性能优化时,傻傻分不清广播和服务超时的区别,结果查了一天发现方向就错了……嗯,现在我把经验直接整理给你。
一句话总结:ANR 本质是主线程被阻塞太久没响应系统请求。但不同请求的“忍耐时间”不同,表现也不同。
先看一张分类结构图
下面这张图梳理了四大类型的归属关系和典型场景,建议先有个整体印象——
1. 输入事件 ANR
这是用户感知最强的 ANR。你想想看,点了一个按钮,5 秒没反应——用户直接就想卸载了。系统里叫 InputEvent 超时,监控的是触摸、按键这类事件的分发。
触发条件:事件在 InputDispatcher 里排队超过 5 秒没被主线程取走处理。其实从 log 里能看到 Input event dispatching timed out 这个关键字。
我在项目里遇过最典型的案例:某个版本上,用户反馈“App 点不动了”,但手机没死机。一查 log,发现主线程在等一个 SharedPreferences 的 apply() 落盘——因为存储 IO 卡住,主线程的 Looper 被堵死了。你说冤不冤?一个 I/O 操作把整个 UI 拖垮了。
避坑小贴士:输入事件 ANR 的堆栈通常停在 InputMethodManager 或 View.dispatchTouchEvent 附近,但真正的原因要从主线程堆栈的 最深处 找。我曾经被表面堆栈骗过两次,后来养成了看“线程状态+锁信息”的习惯。
关键特征:
- 超时时间固定 5 秒(不可配置)
- 只发生在 前台进程(后台不派发输入事件)
- trace 文件中必有
InputDispatcher相关调用
2. 广播 ANR
广播是 Android 里非常灵活的消息机制,但也是最容易出 ANR 的类型之一。关键区别在于 前台广播 和 后台广播 的超时时间完全不同。
超时值:
| 广播类型 | 超时时间 | 说明 |
|---|---|---|
| 前台广播 (FLAG_RECEIVER_FOREGROUND) | 10 秒 | 高优先级,比如系统关键事件 |
| 后台广播 | 60 秒 | 普通应用广播,超时宽松 |
我记得有一次排查一个线上崩溃,发现某些低端机频繁出广播 ANR。分析后发现:第三方广告 SDK 在 onReceive() 里直接做了网络请求——网络慢的时候等待 15 秒,前台广播直接超时。解决方案?其实很简单,把耗时操作挪到 IntentService 或者用 WorkManager 就行。但很多人就是懒得改。
注意:从 Android 8.0(API 26)开始,大多数隐式广播不能注册在 manifest 里了。但显式广播和系统广播(如 CONNECTIVITY_CHANGE)仍然会导致 ANR。我曾经见过一个 App 在 ACTION_POWER_CONNECTED 里做数据库迁移——5 秒没跑完挂了前台广播。
排查要点:
- 看 logcat 中的
BroadcastQueue日志,找到哪个BroadcastReceiver超时 - 在 trace 里找
handleReceiver或onReceive调用栈 - 检查
onReceive()里是否直接调用了ContentResolver、网络、文件操作
3. 服务 ANR
服务的 ANR 分为两类:启动超时 和 连接超时。服务的 onCreate()、onStartCommand()、onBind() 这些生命周期方法如果执行太久,系统就会直接炸。
超时时间差异巨大——前台服务 20 秒,后台服务 200 秒。说白了,系统也知道后台服务有可能做点重活。但 200 秒你想想,都三分钟了,说明问题非常严重。
我参与过一个竞品分析项目,对方 App 后台下载逻辑写在 onStartCommand() 里,一边下载一边写文件,结果网络波动时卡了 30 多秒。虽然是后台服务,没触发 ANR,但导致 Service 被系统直接杀掉——任务丢了一半。后来我们改用 DownloadManager,稳多了。
经验之谈:服务 ANR 的堆栈通常停在 ApplicationThread.scheduleServiceArgs 或 ActivityThread.handleServiceArgs 上。关键看 主线程 当时在干什么。如果发现主线程在等一个 CountDownLatch 或 synchronized 锁,大概率是死锁或锁竞争。
4. ContentProvider ANR
这是最容易被忽视的类型。ContentProvider 本身的 onCreate() 有 10 秒超时,另外 query()、insert()、update()、delete() 操作如果跑在主线程(比如通过 Activity 直接调用),也可能阻塞 UI。
但还有一个隐藏场景:跨进程 ContentProvider。A 进程请求 B 进程的 Provider,B 进程的主线程如果卡死,A 进程也会跟着 ANR——因为 Binder 调用在等待返回。这种“传染”问题我在多媒体 App 里见过好几次,一个媒体 Provider 卡死,整个 MediaBrowserService 都瘫了。
排查方法:
- logcat 搜
ContentProvider或timeout - trace 找
ContentProviderProxy相关栈,看哪个 Provider 没响应 - 检查 Provider 是否在主线程做数据库升级(
onUpgrade)——这个操作默认跑在 Provider 的线程,但如果你在SQLiteOpenHelper里没处理并发,可能会阻塞
小技巧:给 ContentProvider 的耗时操作加个 AsyncTask 或 HandlerThread,并且把数据库操作放到后台线程。另外,我建议在 Application.onCreate() 里尽早初始化 Provider 需要的资源,避免 onCreate 超时——尤其是当其他进程启动时饿汉式加载 Provider。
四种 ANR 对比速查表
| 类型 | 超时时间 | 出现场景 | 常见根因 | trace 关键字 |
|---|---|---|---|---|
| 输入事件 ANR | 5 s | 触摸/按键无响应 | 主线程被 I/O 或锁阻塞 | InputDispatcher |
| 广播 ANR(前台) | 10 s | 系统广播、自定义广播 | onReceive 执行耗时操作 | BroadcastQueue |
| 广播 ANR(后台) | 60 s | 隐式广播 | 数据库/网络操作 | BroadcastQueue |
| 服务 ANR(前台) | 20 s | Service.onCreate / onStartCommand | 主线程中文件/网络操作 | handleServiceArgs |
| 服务 ANR(后台) | 200 s | 后台 Service 生命周期 | 死锁、长时计算 | handleServiceArgs |
| ContentProvider ANR | 10 s | Provider.onCreate / CRUD | 数据库迁移、跨进程 Binder 阻塞 | ContentProviderProxy |
最后多说一句:不管哪种类型,trace 文件里的信息永远是最可靠的。我习惯先定位主线程的锁状态和阻塞点,再用 CPU 采样 看方法耗时分布。这个习惯帮我在一周内处理了 30 多个线上 ANR 工单——真的,比盲目看 logcat 有效多了。