3. ANR的类型分类

搞 ANR 监控这么多年,我一直觉得:不懂类型,没法定位。很多人拿到 log 就慌,其实只要看懂了是哪种 ANR,排查方向就清晰一半。今天咱们就把这四种类型掰开揉碎聊聊。

系统把 ANR 分成四大类:输入事件、广播、服务、ContentProvider。每种触发条件、超时时间、监控手段都不一样。我刚开始做性能优化时,傻傻分不清广播和服务超时的区别,结果查了一天发现方向就错了……嗯,现在我把经验直接整理给你。

一句话总结:ANR 本质是主线程被阻塞太久没响应系统请求。但不同请求的“忍耐时间”不同,表现也不同。

先看一张分类结构图

下面这张图梳理了四大类型的归属关系和典型场景,建议先有个整体印象——

ANR 类型分类结构图 ANR 类型 输入事件 ANR 超时 5s | 点击/触摸无响应 广播 ANR 前台 10s / 后台 60s 服务 ANR 前台 20s / 后台 200s ContentProvider ANR 超时 10s | 查询/更新阻塞 切记:所有 ANR 都发生在主线程。但系统对每种请求的“容忍度”不同,超时时间是关键区别。 输入事件 ANR 最敏感(5 秒),后台服务最宽松(200 秒)

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 秒没跑完挂了前台广播。

排查要点:

  1. 看 logcat 中的 BroadcastQueue 日志,找到哪个 BroadcastReceiver 超时
  2. 在 trace 里找 handleReceiver 或 onReceive 调用栈
  3. 检查 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 关键字
输入事件 ANR5 s触摸/按键无响应主线程被 I/O 或锁阻塞InputDispatcher
广播 ANR(前台)10 s系统广播、自定义广播onReceive 执行耗时操作BroadcastQueue
广播 ANR(后台)60 s隐式广播数据库/网络操作BroadcastQueue
服务 ANR(前台)20 sService.onCreate / onStartCommand主线程中文件/网络操作handleServiceArgs
服务 ANR(后台)200 s后台 Service 生命周期死锁、长时计算handleServiceArgs
ContentProvider ANR10 sProvider.onCreate / CRUD数据库迁移、跨进程 Binder 阻塞ContentProviderProxy

最后多说一句:不管哪种类型,trace 文件里的信息永远是最可靠的。我习惯先定位主线程的锁状态和阻塞点,再用 CPU 采样 看方法耗时分布。这个习惯帮我在一周内处理了 30 多个线上 ANR 工单——真的,比盲目看 logcat 有效多了。