2、Android ANR 机制剖析:ANR 的定义、触发条件、如何查看 ANR 日志
什么是 ANR?
ANR 全称 Application Not Responding,说白了就是应用无响应。你想想看,用户点了一个按钮,等了 5 秒界面还没反应,系统就会弹出一个「应用未响应」的对话框,问用户是要继续等还是直接关掉。
我记得第一次看到这个弹窗时还挺慌的,后来才明白,这其实是 Android 系统的一种保护机制——它要保证用户永远不被一个卡死的应用绑架。
触发条件:系统什么时候会判 ANR?
系统会盯着几个关键的超时阈值,一旦主线程在规定时间内没处理完特定事件,就会判定 ANR。我个人习惯把触发的场景分成以下四类:
| 触发场景 | 超时类型 | 典型阈值 |
|---|---|---|
| 输入事件 | 按键/触摸事件派发超时 | 5 秒 |
| 广播接收 | 前台广播 onReceive 超时 | 10 秒 |
| 广播接收 | 后台广播 onReceive 超时 | 60 秒 |
| 服务 | Service 的生命周期方法超时 | 20 秒 |
| 内容提供者 | ContentProvider 发布/查询超时 | 10 秒 |
我在项目中遇到过最典型的就是输入事件超时:主线程里做了网络请求,或者直接跑了一个大文件的加解密操作,5 秒没处理完,ANR 立刻就来。
如何查看 ANR 日志?
ANR 发生之后,系统会生成一份详细的日志,存到 /data/anr/traces.txt 里。怎么抓?我一般用 adb 直接拉出来:
adb root
adb shell ls /data/anr/ # 看有没有 traces 文件
adb pull /data/anr/traces.txt ./
打开 traces.txt,你首先会看到一行粗体标题「ANR in …」,后面跟着发生 ANR 的包名和进程 ID。往下翻,最重要的部分是每个线程的堆栈信息——尤其是「main」线程。
grep 过滤关键词 "main" 或者 "at",快速定位到卡死的调用链。
ANR 日志长什么样?
举个例子,这是我们项目线上抓到的真实日志片段:
"main" prio=5 tid=1 Blocked
at com.example.app.KeyStoreHelper.sign(KeyStoreHelper.java:42)
- waiting to lock <0x0c4d2b40> (a android.security.keystore.AndroidKeyStoreProvider)
at com.example.app.MainActivity.onClick(MainActivity.java:30)
...
看到「Blocked」和「waiting to lock」没?这基本就是 Keystore 操作卡住主线程的铁证。我当初排查时就是靠这一行锁定了问题——原来 Keystore 的 sign() 方法在内部做了一次耗时几十毫秒的 IO,但因为主线程被其他任务占着,叠加起来就超过了 5 秒。
ANR 的完整流程
系统从检测到 ANR 到最后生成日志,其实是一个严谨的流程。我画了一张图帮你理解:
核心知识点:ANR 的本质是系统对主线程响应能力的一种「健康检查」。只要主线程在关键超时窗口内无法处理事件,就会被判定为 ANR,跟应用是否真正崩溃无关。很多开发者以为 ANR 是偶发 bug,其实只要我们能把耗时操作扔出主线程,ANR 就能从根源上避免。
避坑指南:我曾经在集成某个第三方的 Keystore 封装库时,它的 sign() 方法内部没有做异步处理,导致主线程直接调用了底层 C++ 的加密计算。虽然只有几十毫秒,但叠加了主线程原有的 UI 绘制和事件分发,最终就踩到了 5 秒的红线。所以——任何可能阻塞主线程的 I/O、计算、加解密操作,都一定要异步化。
怎样快速定位 ANR 根因?
- 看主线程堆栈 —— 搜索
"main"线程,找到卡住的调用链。 - 看锁等待 —— 如果出现
waiting to lock,说明在等一把锁,顺着锁的地址找到哪个线程持有它。 - 看 CPU/IO —— traces.txt 头部有 CPU 负载、磁盘 IO 的统计,可以辅助判断是否系统资源不足导致主线程被饿死。
举个例子,有一次我查到一个 ANR 日志里主线程在 Thread.sleep(),sleep 了 8 秒。你想想看,哪个正经业务会在主线程睡 8 秒?原来是一个测试同事随手写了个调试代码没删掉。嗯,遇到这种奇葩情况,日志一看就懂了。