第2章:Android ANR机制详解

说起ANR,很多同学第一反应是「应用无响应弹窗」。但你真的理解它背后的触发逻辑吗?我个人习惯先把底层机制摸透,再去排查问题。这一章我们就来扒一扒ANR的“底裤”。

2.1 什么是ANR?

ANR全称Application Not Responding,系统认为你的应用“没救了”就会弹出这个对话框。说白了,就是Android系统给你设置了一个超时闹钟,到了时间你还没干完活,系统就判定你阻塞了主线程。

场景超时阈值常见原因
输入事件(按键、触摸)5秒主线程耗时操作,如网络请求
广播接收(BroadcastReceiver)前台10秒 / 后台60秒onReceive里做大量计算或I/O
服务(Service)前台20秒 / 后台200秒服务里执行耗时长任务
ContentProvider10秒查询或写入超时

我曾在一个直播项目里遇到过,用户快速滑动列表时偶尔弹出ANR。排查后发现是某个列表项加载图片时做了同步解码,卡了主线程。嗯,那时候我才对「5秒」这个阈值有了血泪般的认识。

2.2 ANR的触发流程

系统是怎么知道你“无响应”的?我给大家拆解一下内部流程。其实核心就是三个角色的配合:监控者、调度者、执行者。

核心思路:系统在消息队列里塞了一个“超时检测”的屏障消息,如果屏障消息在规定时间没被消费,就会拉起ANR。

为什么会这样?我们看一下消息队列的套路:

  1. 发送消息:当要处理输入事件或广播时,系统往主线程Looper里发送一个普通消息,同时启动一个超时计时器。
  2. 超时计时:计时器的时间就是上面表格里的阈值。计时器到点后,会检查该消息是否已被处理。
  3. 触发判罚:如果消息还在队列里没被处理,说明主线程卡住了。系统就会向系统服务(例如ActivityManagerService)发送一个“ANR触发”的信号。
  4. 生成ANR:AMS收到信号后,会采集当前进程的线程堆栈、CPU使用率等信息,然后弹出ANR对话框,同时写入traces.txt。
// 伪代码示意:超时检测机制
void dispatchMessage(Message msg) {
    startTimeout(msg, 5000); // 启动5秒超时
    msg.target.handleMessage(msg);
    cancelTimeout(msg);
}

你看,如果 handleMessage() 里执行了耗时任务,那 cancelTimeout() 就会延迟调用,超时机制自然就触发了。

2.3 主线程为什么会被卡住?

主线程卡住的根因是什么?本质上就是Looper循环卡住了,无法继续处理下一个消息。我总结了三个最常见的“凶手”:

  • 同步I/O操作:比如在UI线程里读写文件、执行网络请求。哪怕是OkHttp的同步请求,也不能放在主线程。
  • 长时间计算:比如Bitmap缩放、JSON解析过大列表。我记得有一次在播放器项目里,把视频缩略图生成放在了主线程,结果滑动seekBar就ANR了。
  • 锁竞争导致死锁或活锁:主线程等一个锁,而持有锁的线程又在等主线程去处理某个回调。经典的死锁场景。
我的一个小习惯:排查问题时先看/data/anr/traces.txt,里面会明确记录主线程的堆栈和锁状态。如果你看到“waiting to lock”或“blocked on <0x..>”,基本就是锁的问题了。

2.4 ANR的判定维度

系统不是看到主线程卡了就立马弹框,它还要综合几个维度判断:

维度说明举例
消息处理超时消息从入队到出队超过阈值触摸事件5秒未响应
CPU占用率如果CPU空闲,系统可能放你一马系统认为当前无负载压力
进程是否被杀如果进程已经被kill,就不会弹ANR低内存场景
用户感知度只有前台可见或可交互的进程才触发后台进程超时但可能不弹框

你看,系统其实也不是很“严格”。但做播放器的时候,一旦出现ANR,用户就会直接卸载。所以,我们还是要彻底杜绝。

2.5 本章知识体系图

下面这张图总结了ANR的触发链条和关键节点。建议大家对照着记忆,排查问题的时候心里就有地图了。

ANR 触发机制核心流程 主线程(UI线程) 负责处理消息队列 消息队列(MessageQueue) 普通消息 + 屏障消息 超时监控(Timeout Checker) 启动屏障消息 + 计时器 AMS (ActivityManagerService) 接收ANR信号,采集堆栈 生成ANR 弹对话框 + 写traces 判罚维度 • 消息超时 • CPU 占用率 • 进程是否存活 • 用户感知度 常见卡顿原因 • 同步I/O操作 • 长时间计算 • 锁竞争/死锁 • Binder调用阻塞 用户看到“应用无响应”

2.6 避坑指南

我曾经在优化ExoPlayer播放器时,发现一个诡异的ANR:每次切码率时就会卡住。排查发现是ExoPlayer在主线程进行MediaCodec配置时调用了 MediaCodec.configure(),而这个方法在某些设备上可能耗时超过200毫秒。如果连续切换,积累下来就触发了5秒阈值。

教训: 任何时候都不要在主线程做任何可能阻塞的操作。即使ExoPlayer内部的回调也要检查是否跑在子线程。如果发现某些回调默认跑在主线程,一定要通过HandlerThread或自定义线程池跳转。

还有一次,业务方在BroadcastReceiver的 onReceive() 里直接调了ExoPlayer的 prepare() 并等待完成。这一等就是十几秒,ANR稳稳地被触发。其实只要把 prepare() 扔到后台线程,问题就解决了。

小技巧: 在AndroidManifest里给ExoPlayer的播放Service声明 android:process=":player" 可以隔离到独立进程,这样主进程的ANR就不会影响到播放流程。但要注意跨进程通信的开销。