4、ANR触发流程:系统如何检测ANR

ANR这东西,说白了就是系统给你发的一张「红牌」。你主线程卡太久,它就鸣哨判罚。很多朋友只看过ANR弹窗,但不知道背后系统是怎么一步步检测、判断、最终弹出那个烦人对话框的。今天我就把整个链路拆开来讲,顺便聊聊Service超时和Broadcast超时到底有什么不一样。

4.1 系统检测ANR的核心机制

我刚开始做Android开发时,一直以为系统是通过某种「实时监控」来发现ANR的。后来翻源码才发现——其实用的是「定时炸弹」的方式。

具体来说,系统会为每个可能发生ANR的场景设置一个超时定时器。比如你发了一个广播,系统就倒计时10秒。如果你在规定时间内没处理完,定时器一响,系统就判定ANR。

举个例子,你点了一个按钮,Activity开始做耗时操作。这时候系统在等待你响应输入事件,它会给你的主线程发一个「信号」,同时启动一个5秒的定时器。如果5秒内主线程没把信号处理完(比如卡在数据库查询上),定时器就爆炸了。

关键点: ANR检测不是「一直盯着你看」,而是「设一个Deadline,到点没完成就判负」。

4.2 ANR对话框何时弹出?

「弹出对话框」和「系统判定ANR」其实有细微的时间差。系统判定ANR后,会先杀掉对应进程吗?不会。它会先收集现场信息——抓取当前所有线程的堆栈、CPU使用率、进程状态等等,然后跳出一个对话框,让你选择「等待」还是「关闭」。

这个对话框是由系统的 ANRManager 中的 appNotResponding() 方法触发的。我这里列一下它的大致处理流程:

  1. 系统发现超时,调用 appNotResponding()。
  2. 收集现场信息(堆栈、负载、内存等)并写入 /data/anr/traces.txt。
  3. 弹出ANR对话框(注意:这里不会立即杀进程,给你一次机会点「等待」)。
  4. 如果用户点击「关闭」,或者用户不操作一段时间后,系统才杀掉进程。

我遇到过一种情况:某App在后台做同步,前台用户没操作,系统检测到ANR但用户没看到弹窗——因为系统只在用户当前交互的App上弹ANR对话框。后台进程被判定ANR后,系统只会记录日志并直接杀掉,不弹窗。

避坑: 我曾经遇到过线上ANR率特别高,但用户反馈很少。后来查日志才发现大量Service超时ANR发生在后台,用户根本没看到弹窗。所以光靠用户反馈找ANR是不靠谱的,必须从日志和监控系统里抓。

4.3 Service Timeout 与 Broadcast Timeout 的区别

很多团队把Service和Broadcast的超时混为一谈,但它们其实差异很大。我整理了一个对比表格:

对比维度 Service Timeout Broadcast Timeout
超时时长 前台Service 20秒,后台Service 200秒 前台广播10秒,后台广播60秒(Android 14已收紧)
触发条件 onStartCommand() 或 onBind() 执行时间过长 onReceive() 方法执行时间过长
是否弹窗 Service运行在前台时弹窗,后台时只记录日志 广播接收者在当前焦点App时弹窗,后台时只记录日志
系统处理方式 超时后先警告,再杀进程 超时后直接杀进程(较激进)
典型场景 Service里做网络请求、数据库批量写入 BroadcastReceiver里调了耗时API(如网络、文件IO)
常见误用 在Service里启动一个线程就不管了 在onReceive里弹Dialog、启动Activity

你有没有注意到,Service的Timeout其实比Broadcast宽容很多。为什么?因为Service是专门跑后台任务的组件,系统给了更多时间。而BroadcastReceiver设计上就是「轻量、快速执行」,你不该在里面做任何超过几毫秒的事。

我记得有一次排查一个ANR,发现是BroadcastReceiver里调了一个耗时的网络接口,然后还等回调。哦对,onReceive() 是在主线程执行的,你一阻塞,整个App界面都卡死了。

4.4 完整ANR触发流程图

下面这张图我帮你把整个流程串起来了,从「超时定时器启动」到「弹窗」或「直接杀进程」,一目了然。

ANR 检测触发流程图 启动超时定时器 定时器倒计时结束? 是(超时) 系统判定ANR成立 收集堆栈 & 写入 traces.txt 当前组件在前台? 是 弹出ANR对话框 否 直接杀进程(记录日志) 用户选择 等待 / 关闭 杀进程

4.5 不同超时类型的细节差异

其实Service超时还分「前台」和「后台」。前台Service有20秒的窗口,后台Service能达到200秒——但后台Service从Android 8开始限制更严了,你就算给200秒,系统也可能提前杀你。我个人习惯是把所有长时间任务扔到一个独立的后台线程,不在组件的生命周期回调里做重活。

Broadcast超时就更敏感了。Android 14把后台广播超时时间从60秒降到了10秒,跟前台一样。你想想看,10秒转瞬即逝,稍微做一个网络请求就超了。我建议所有广播接收者里只用 intent.putExtra() + startService(),即收即走。

技巧: 如果你实在需要在BroadcastReceiver里做耗时操作,可以调用 goAsync() 创建一个异步接收。但注意,异步模式下的超时时间也是有限制的,只是不会卡主线程。我记得有个库就是用 goAsync() 来规避主线程阻塞,不过要小心内存泄漏。

4.6 实战案例:一次诡异的Service Timeout ANR

我终于可以讲个亲身经历了。有一次线上报告ANR率突然升高,全是 android.app.ServiceTimeout。我查了 traces.txt,发现主线程卡在 BinderProxy.transact() 上。大家第一反应是「Binder死锁了」,后来才发现不是。

问题出在App启动时,一个后台Service调用了 startForeground(),但是通知渠道因为某些手机厂商的权限限制没创建成功。导致 startForeground() 抛了异常,Service退回到后台模式。而Service又在 onStartCommand() 里做了一次远程Binder调用,远程服务正好在另一个进程里卡住了。这一套组合拳下来,Service花了30秒都没跑完,而前台Service的超时只有20秒……

解决方式也很简单:在 onStartCommand() 开头就判断通知渠道创建状态,没成功就降级为 startService() 然后提前返回,把真正的任务交给工作线程。

教训: Service超时常常不是单点问题,而是多个因素叠加导致的。排查时一定要看整体链路:有没有跨进程Binder调用?有没有加锁阻塞?有没有异常导致降级?我后来给团队写了一个Service超时排查清单,效果不错。

4.7 总结几条实测经验

  • 弹窗不是必须的: 后台ANR大部分只在日志里留下痕迹,用户看不到。所以监控系统必须从日志里捞ANR事件,不能只靠用户反馈。
  • 超时时间是灵活的: 不同Android版本、不同厂商ROM可能调整了超时值。我见过某厂商把前台广播超时改到了5秒,导致很多兼容性问题。
  • Service尽量用前台进程: 如果有持续后台任务,老老实实弹前台通知。否则系统随时可能因为超时或低内存杀掉你,ANR都不给你弹窗。
  • 广播用LocalBroadcastManager(现在推荐WatchableLiveData或Flow)替代: 能避免很多跨进程超时的问题。

嗯,关于ANR的触发流程就说这么多。记住那条时间线:超时→判定→收集信息→(前台弹窗/后台杀进程)。搞清楚了这条线,以后看到ANR日志就不会一脸懵了。