2、ANR触发机制:系统如何检测ANR、主线程消息循环与Looper原理

做ANR分析,第一件事就是要搞懂系统是怎么知道你卡住的。说白了,Android系统就像个严厉的监考老师,你主线程长时间不交卷(不处理消息),它就给你记过——弹出ANR对话框。

这一节,我带你看清楚系统检测ANR的整套流程,以及Looper的消息循环机制。搞懂这两块,你才能知道ANR到底是怎么被“揪出来”的。

2.1 系统检测ANR:谁在背后盯着你?

Android系统底层有一个“Watchdog”机制,它专门监控主线程的响应情况。具体来说,主要分为三类角色:

角色检测内容超时时间
Service服务前台服务20秒,后台服务200秒20s / 200s
Broadcast广播前台广播10秒,后台广播60秒10s / 60s
Input事件触摸/按键事件5秒无响应5s

这里插一句我个人的经验——很多同学以为只有点不动屏幕才会触发ANR。其实Service超时也是常见原因,尤其后台服务200秒窗口很大,但一旦后台任务阻塞主线程,照样炸。我在项目里至少见过三次,是因为ContentProvider初始化时在主线程做了网络请求,直接引发Service ANR。

2.2 Looper与消息循环:主线程的“心脏”

主线程之所以能处理各种事件,全靠Looper在背后驱动消息循环。我们先拆开来看:

  • Looper:负责从消息队列中取消息,并派发给目标Handler处理
  • MessageQueue:存放待处理消息的队列,底层是native的epoll机制
  • Handler:发送消息和接收处理回调

嗯,这里要注意:主线程的Looper是在ActivityThread.main()里通过Looper.prepareMainLooper()创建的。创建后立即调用Looper.loop()进入死循环,不断从MessageQueue里取消息。

代码长这样:

public static void main(String[] args) {
    Looper.prepareMainLooper();  // 创建主线程Looper
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainLooper != null) {
        sMainLooper.setMessageLogging(...);
    }
    Looper.loop();  // 进入消息循环,永不返回
}

为什么Looper.loop()不会卡死?因为当消息队列为空时,它会阻塞在nativePollOnce()里,等待下一个消息到来时通过epoll唤醒。这时候CPU不会空转,所以不影响功耗。

我的一个小技巧:在日常开发中,如果想观测主线程消息处理耗时,可以给Looper设置一个Printer。我在定位一次卡顿问题时,就靠打印每条消息的前后时间戳,精确找到了一个View绘制方法里偷偷做了Bitmap解码——耗时100多毫秒,才导致用户感觉卡。

2.3 系统如何精准判断“超时”

你以为系统只是简单记个时?没那么简单。它走了一套严谨的“三段式”流程:

  1. 发送延时消息:当关键事件发生时(比如Input事件分发),AMS会向自己发一个延时5秒的ANR消息。
  2. 清除标记:如果主线程在规定时间内正常处理完事件,系统就会把这个延时消息从队列中移除。这叫“取消ANR”。
  3. 触发ANR:如果延时消息到时间了还没被移除,AMS就会执行真正的ANR流程——杀掉进程、生成trace文件、弹Dialog。

这个机制有个重要细节:必须由主线程主动通知系统“我完成了”。比如Input事件处理完后,系统会调用InputChannel的finished信号。如果主线程因为死锁或长时间sleep,没机会执行这个信号,那系统就只能等着超时。

我曾经踩过的坑:有一次线上ANR,看trace文件发现主线程停在FileOutputStream.write()上,持续了40秒。当时同事都怀疑是外置存储有问题。后来仔细排查才发现,是因为一个第三方SDK在子线程里频繁写日志,把文件锁占住了——主线程的写入请求一直等锁。所以记住:主线程不仅不能做耗时操作,还要警惕子线程持锁竞争。

2.4 ANR检测链路的完整流程

系统检测ANR的全路径,我画了一张图帮你串起来:

ANR检测链路核心流程 事件到达(Input/Service/Broadcast) AMS发送延时ANR消息(5s/10s/20s) 主线程及时处理完毕 系统移除延时消息 → 无事发生 延时消息到期仍未移除 触发ANR流程 1. 生成traces.txt 2. 弹ANR对话框 3. 决定是否杀掉进程 Input事件 额外记录用户操作 说明:ANR检测本质是"定时炸弹"模式——发出延时消息,处理完成则拆弹,超时不拆则引爆。 主线程被阻塞的源头可能是:耗时计算、IO、死锁、Binder同步调用等。

2.5 Looper.loop() 内部到底干了什么?

我们再深入一下Looper.loop()的核心逻辑。伪代码可以简化为:

public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    for (;;) {
        Message msg = queue.next(); // 可能会阻塞在这里
        if (msg == null) return;    // 只有退出时才会走到这里
        msg.target.dispatchMessage(msg); // 交给对应Handler处理
        msg.recycleUnchecked();
    }
}

关键就在 queue.next() 这个方法。它内部会调用 nativePollOnce() 来挂起当前线程,直到下一个消息的触发时间到了,或者有新的消息入队。这期间线程是让出CPU的,不会占用资源。

那么问题来了——如果主线程在dispatchMessage()里执行了一个耗时5秒的操作,会发生什么? 系统会在这5秒内无法处理任何其他消息(包括绘制、输入事件)。如果这时候用户点击了屏幕,Input事件就会排队等待。5秒后,系统发现输入响应超时,就会触发ANR。

2.6 为什么有时候不弹ANR对话框?

你可能见过这种情况:应用卡了好久,但最后没弹出ANR对话框,直接黑屏或闪退了。其实系统在某些条件下可以不弹框直接杀进程:

  • 如果进程正处于后台,系统可能直接kill而不告知用户
  • 如果连续多次ANR,系统会进入“沉默模式”,只记录日志不弹框
  • 某些系统定制ROM会修改默认行为
排查建议:当线上出现“无ANR弹窗但应用闪退”时,一定要去查看 /data/anr/traces.txt 或 dropbox 日志。很多系统在杀进程前还是会写trace文件的。我遇到过多次,用户投诉闪退,结果一看traces文件,主线程全停在Thread.sleep()上——原来是开发测试用的调试代码忘记删了。

2.7 总结一下核心要点

我帮你把这一章的重点捋一遍:

  1. ANR是系统主动检测出来的,不是用户反馈的。核心就是“延时消息”+“超时未取消”。
  2. 主线程消息循环由Looper驱动,它一个接一个地处理Message。任何消息耗时过长都会阻塞后续消息,包括Input、绘制、生命周期等。
  3. 系统对Service、Broadcast、Input设置了不同的超时时间,不要把以前做后台Task的思路直接搬到主线程上。
  4. Looper.loop()里的queue.next()会阻塞空闲线程,但dispatchMessage()是真正可能卡住的地方。
  5. 不要忽略“沉默杀”的情况——没弹ANR框不代表没问题,该看traces还是得看。