第4章 ANR基础:ANR产生原因与系统机制归纳总结
做 Android 开发,最怕看到弹窗“xxx 无响应”。嗯,就是 ANR(Application Not Responding)。说实话,这玩意儿比 Crash 更头疼——Crash 死得痛快,ANR 是拖着系统一起难受。
我有一次上线前,测试小姐姐随手点了个设置页,直接 ANR。排查下来是主线程读了一百多兆本地数据库。那之后我就养成了习惯:所有可能超过 100ms 的操作,必须扔子线程。
这一章咱们就把 ANR 的根因和系统机制拆开看看。你搞懂了它,才能和 Crash 联动监控。
什么是 ANR?
Android 系统给每个应用的主线程设了个“红线”——超过一定时间没处理完输入事件(按键、触摸)、广播接收器、Service 启动等,系统就认为你“饿死”了,直接弹 ANR 对话框。说白了,就是你让用户等了太久。
核心结论: ANR 本质上是一种超时保护机制。系统通过 watchdog 线程定期检查主线程状态,超时就 dump trace 并弹窗。
ANR 的三大触发场景
系统对不同组件设了不同的超时阈值,我整理了一张表,你平时排查对照着看:
| 场景 | 触发条件 | 默认超时时间 | 关键点 |
|---|---|---|---|
| 输入事件 | 按键/触摸事件超过 5s 未分发给 Activity | 5s | 通常伴随 InputDispatcher 日志 |
| 广播接收器 | BroadcastReceiver.onReceive() 未在超时内返回 | 前台 10s / 后台 60s | 留意 "Broadcast of Intent" 超时日志 |
| Service | Service.onCreate()、onStartCommand() 或 onBind() 超时 | 20s | ActivityManager 会主动杀死 Service 进程 |
避坑指南: 我曾经接手过一台定制 ROM 的手机,广播超时被厂商改成了 30s。所以别完全相信官方时间,线上要以日志为准。
ANR 产生的真正原因
表面看是主线程“被卡住”,实际原因就那几类。我把最常见的情况列出来,你对照着看自己项目有没有踩过坑:
- IO 操作卡主线程 —— 读大文件、写 SQLite、网络请求。我有次一个同事在 Activity.onCreate() 里直接跑 Glide 加载图片缓存,结果图片很大,主线程等文件缓存等了 4 秒多,直接 ANR。
- 锁竞争导致死锁或长时间等待 —— 主线程持有一个锁,然后去等子线程释放另一个锁。典型的死锁场景。我记得自己写过一段代码,主线程 synchronized 一个对象,子线程也要锁这个对象,结果子线程耗时长,主线程直接挂起。
- Binder 调用阻塞 —— 主线程调用系统服务(如 getInstalledPackages)或者跨进程通信,对方没响应。比如你调了 PackageManager.queryIntentActivities(),系统服务那边处理慢,主线程就卡住了。
- 主线程触发了 GC 且无法抢占 —— 内存紧张时 GC 在大堆上花了好几百毫秒。这种情况其实少见,但一旦发生就很难排查。
- UI 线程逐帧处理量太大 —— 不仅仅是绘制,布局、inflate、动画回调里做了复杂计算。比如在 onDraw 里写了个 O(n²) 的循环。
注意: 不是说所有主线程耗时都会 ANR。系统只检测“输入事件分发”和特定组件生命周期。如果只是主线程跑了个耗时循环但没触发输入事件,可能不弹 ANR,但用户体验照样很差。所以我们做联动监控时,不能光看 ANR 日志,还得结合卡顿监控(FrameDrop)。
系统 ANR 检测机制
系统里具体是怎么检测的?我画了一张图,帮你理清整个链路:
如何拿到 ANR 的 trace?
系统在 ANR 触发后会向 /data/anr/traces.txt 写入所有线程的堆栈。你可以通过 adb 直接拉取:
adb root
adb shell ls /data/anr/
adb pull /data/anr/traces.txt .
但要注意,线上用户手机通常拿不到 root 权限。那怎么办?我一般用这两个方案:
- 集成第三方崩溃 SDK:Firebase Crashlytics、Bugly 这些都会自动捕获 ANR 的 main 线程堆栈,并上传到后台。
- 自己写 FileObserver:监听 /data/anr 目录变化。不过这个方案需要系统权限,普通进程监听不到。我曾经跟厂商 ROM 团队合作,让系统 ANR 后自动把 traces.txt 拷贝到 app 私有目录。
个人经验: 在 Android 10 之后,系统对 ANR 的日志输出做了调整,部分厂商还会混淆堆栈函数名。建议你同时收集 “CPU usage” 和 “load average” 信息,这些在 ANR 日志里也会附带,能帮你判断是资源竞争还是死锁。
总结归纳:ANR 根因与应对思路
你看完上面的内容,心里应该有个谱了。我快速总结成三点,方便你记忆:
- 原因三件套: IO、锁、Binder 阻塞。其他都是这三大类的变种。
- 检测靠超时: 输入 5s、广播 10s/60s、Service 20s。每个超时对应不同的 watchdog 线程。
- 监控要联动: ANR 日志和卡顿监控(block canary / FrameCallback)双管齐下。很多 ANR 其实是卡顿累积触发的。
我记得有一次线上 ANR 飙升,查 traces.txt 发现主线程在等一个 Binder 锁,而那个 Binder 服务是系统级的粘性服务,被另一个进程占着不释放。最后我们通过 Hook 系统 Binder 线程池才定位到问题。所以,搞懂系统机制,才能灵活做监控。