ANR 基础概念与分类归纳

大家好,欢迎来到这门实操课程。很多同学一听到 ANR 就头大,其实它没那么神秘。咱们今天先把地基打牢。

ANR 是什么?Application Not Responding,应用无响应。说白了,就是系统觉得你的 App 卡死了,弹个对话框问用户「是继续等还是关掉?」。用户看到这个,大概率会选择「关闭」。所以,ANR 是用户体验的头号杀手。

核心定义: 当应用的 UI 线程被阻塞超过一定时间,系统就会触发 ANR。

触发 ANR 的四大场景

我刚开始做 Android 时,以为只有主线程做耗时操作才会 ANR。后来发现,场景远比想象的多。系统对不同类型的操作,容忍时间是不同的。嗯,这点很关键。

场景类型 超时时间 触发条件
输入事件(Key/Click) 5 秒 主线程未处理完上一个事件
广播接收器(BroadcastReceiver) 前台 10 秒 / 后台 60 秒 onReceive() 方法未返回
服务(Service) 前台 20 秒 / 后台 200 秒 onCreate(), onStartCommand() 等超时
内容提供者(ContentProvider) 10 秒 query(), insert() 等操作超时

系统是如何「盯」着你的

系统内部有一套监控机制。当发生上述操作时,系统会启动一个「倒计时」。倒计时结束如果还没完成,系统就判定你 ANR 了。这就像老师在监考,你必须在规定时间内交卷,否则就算作弊失败。

  • InputDispatcher: 监控输入事件。5 秒内没有新事件派发,就触发。
  • BroadcastQueue: 监控广播。前台 10 秒,后台 60 秒。
  • ServiceRecord: 监控服务。前台 20 秒,后台 200 秒。
  • ProviderRecord: 监控内容提供者。10 秒内必须完成。
警告: 很多同学以为「后台广播可以慢一点」。其实在 Android 12+ 上,后台广播限制非常严格,弄不好就会闪退或者 ANR。

为什么会出现 ANR?

归根结底,就是主线程被「堵死」了。我遇到过最离谱的案例,是一个同事在onClick里直接调用了Thread.sleep(10000)。不用想,必弹 ANR。

常见原因无非这几种:

  1. 主线程做耗时 I/O: 比如直接在主线程读写文件、网络请求、数据库操作。
  2. 锁竞争: 主线程等一个被子线程持有的锁,子线程又在做慢操作。
  3. 死锁: 两个线程互相等待对方释放锁。你等我,我等你,最后一起挂。
  4. 系统 Binder 调用超时: 主线程调用系统服务,比如获取应用列表,系统服务卡顿,主线程也得等着。
一个常见错觉: 很多人觉得「子线程不会导致 ANR」。错!如果子线程持有了主线程需要的锁,子线程慢了,主线程同样会被堵住,最终 ANR。

ANR 的分类归纳

为了方便排查,我把它们归为三类:

  • I/O 类 ANR: 数据库、文件、网络操作在主线程执行。
  • 并发类 ANR: 锁竞争、死锁、同步机制使用不当。
  • 系统类 ANR: 系统服务响应慢、Binder 调用超时、资源耗尽。

这张图能帮你快速定位问题方向。我个人习惯,一看到 ANR 日志,先看它属于哪一类。

ANR 触发与分类流程图 ANR 触发与分类流程图 ANR 触发场景 输入事件 (Key/Click) 超时:5s BroadcastReceiver 超时:前台10s / 后台60s Service 超时:前台20s / 后台200s ContentProvider 超时:10s 系统响应:写入 traces.txt 排查建议 先看 traces 文件 定位主线程堆栈

如何「捕捉」ANR 现场

排查 ANR 最关键的一步,就是拿到案发现场的数据。系统会在 ANR 发生时,把现场快照写到 /data/anr/traces.txt 文件里。这个文件记录了所有线程的堆栈信息。

我建议你养成一个习惯:每次遇到 ANR,第一件事就是拉这个文件。

adb root
adb shell ls -l /data/anr/
adb pull /data/anr/traces.txt ./traces_当前时间.txt
小技巧: 如果你用的是 Android 10+,可以开启「后台 ANR 对话框」开关。在开发者选项里找到「显示所有 ANR」,这样即使用户不操作,后台 ANR 也会弹窗让你看到。

文件内容通常长这样,主线程会有一个明显的 at xxx.xxx.methodName(SourceFile:line) 标记。这就是当时卡住的地方。

----- pid 12345 at 2025-01-01 12:00:00 -----
Cmd line: com.example.app
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x...
  | sysTid=12345 nice=0 cgrp=default sched=0/0
  | heldMutexes=
  at com.example.app.MainActivity.onClick(MainActivity.java:50)
  - waiting to lock <0x...> (a com.example.app.DataManager)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:154)
  at android.app.ActivityThread.main(ActivityThread.java:6117)

看到 Blocked 和 waiting to lock,基本可以断定是锁竞争导致的 ANR。我曾经花了一整天查一个 ANR,就是因为主线程在等一个数据库写锁,而那个锁被子线程持有,子线程又在做网络请求...嗯,说起来都是泪。

总结一下本节课的核心

  • ANR 的本质: 主线程被阻塞,系统不耐烦了。
  • 四大触发场景: 输入、广播、服务、ContentProvider,各自有不同超时时间。
  • 三大分类: I/O 类、并发类、系统类。帮你快速定位方向。
  • 第一手资料: traces.txt 文件,必查!必看!