ANR 基础概念与产生原因

ANR 全称 Application Not Responding,中文叫“应用无响应”。说白了,就是你的 App 卡住了,系统看不下去了,直接弹个对话框问用户“继续等待还是关闭?”

我在项目中见过不少团队,一碰到 ANR 就慌了手脚,只知道抓日志重现场景。其实核心问题很简单:主线程干了不该它干的活。

ANR 的触发规则

系统判定 ANR 有一套硬性超时机制。不同类型的组件,超时时间不一样。我列个表,方便你对照排查:

组件类型超时时间常见场景
Activity(输入事件)5 秒按键、触摸没响应
BroadcastReceiver10 秒(前台广播)
60 秒(后台广播)
onReceive 里做了耗时操作
Service20 秒onStartCommand 或 onCreate 耗时
ContentProvider10 秒query/insert 等操作卡住
注意:从 Android 14 开始,部分广播的后台超时限制更严格,我们做埋点时要特别关注 system_server 的 watchdog 日志。

ANR 产生的根本原因

我习惯把 ANR 原因归纳成四个大类。你想想看,一个 App 的主线程就像一条单车道公路,任何一辆车堵住,后面所有车都得等。

1. 主线程阻塞

这是最常见的原因。主线程做了太多事:网络请求、数据库读写、文件操作、复杂计算……

我曾经接手过一个项目,启动时在 onCreate 里直接调用了 BitmapFactory.decodeFile 加载一张 4K 壁纸,结果 5 秒 ANR 稳定复现。那把图片放到子线程后,问题就解了。

  • 网络调用:HttpURLConnection、OkHttp 同步请求
  • 磁盘 I/O:SharedPreferences 大量写入、SQLite 长事务
  • 锁竞争:主线程等子线程释放锁,比如 Wait、join
  • View 过度绘制:onLayout、onDraw 里做耗时运算

2. Binder 调用超时

主线程调用系统服务(如 ActivityManager、WindowManager)时,如果对方没及时回应,主线程就会被挂起。举个实际例子:

// 主线程调用 ContentResolver
getContentResolver().query(Uri.parse("content://xxx"), null, null, null, null);
// 如果 provider 响应慢,主线程等超过 200ms 即可能触发 ANR

嗯,这里要注意:系统对 Binder 事务有 200ms 的隐式限制,超过这个时间就可能被标记为“卡顿”。

3. 事件分发超时

当用户触摸屏幕或按下按键,InputDispatcher 会往主线程发消息。如果主线程 Looper 里的消息队列积压,5 秒没处理完上个事件,ANR 就来了。

你想想看,如果 onTouchEvent 里做了耗时操作(比如实时滤镜),手指一滑动,马上超时。我曾经见过一个照片编辑 App,滑动亮度滑块时,每次计算直方图用了 2 秒,结果滑动三次就 ANR。

4. 广播 / Service 超时

广播接收器默认在主线程执行 onReceive,你如果在里面做了网络请求或数据库写入,超时就是必然。

  • 静态广播:onReceive 最多 10 秒
  • 动态广播:同样受限于主线程消息队列
  • Service:onStartCommand 20 秒内必须返回
小技巧:对于长时间运行的广播,改用 goAsync() 并在子线程执行,同时持有 PendingResult 超时控制。但要注意及时调用 finish(),否则系统也会降级处理。

ANR 分类图谱

下面的 SVG 图总结了 ANR 的触发来源和核心原因,方便你系统化地记忆——我平时做性能排查时就照着这个思路来:

ANR 产生原因分类框架 Android ANR 主线程阻塞 Binder 超时 事件分发超时 广播 / Service 超时 网络/IO/锁 Binder 阻塞 Input 积压 onReceive 耗时 四种原因均导致主线程 Looper 无法及时处理消息 最终触发系统超时机制 → 弹出 ANR 对话框 个人经验:2023年某电商项目 80% ANR 都出自“主线程阻塞”这类

避坑指南

  • 曾经我遇到一个 case:第三方 SDK 在 onCreate 里用 Handler.postDelayed 做了个 6 秒后的操作,结果这个操作里直接更新 UI,但 Activity 已经进入后台 – 这不是 ANR,却导致系统误判。后来我们加了 Lifecycle 判断才解决。
  • 还有一个坑:SharedPreferences.commit() 是同步写入磁盘,如果数据量大,主线程轻松卡几百毫秒。建议统一用 apply() 或换成 DataStore。
  • 别忘了 Binder 线程池:当系统服务线程池耗尽时,主线程发 Binder 调用也会等。这时候 /proc/self/stat 里看 sched 状态会发现大量 “D” (uninterruptible sleep)。

快速自检清单:

  1. 主线程有没有做网络请求?
  2. 有没有在 UI 线程读写数据库?
  3. 有没有在 onTouchEvent 里做复杂计算?
  4. 有没有使用同步锁并且可能死锁?
  5. BroadcastReceiver 的 onReceive 是否超过 10 秒?