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」线程。

我的小技巧:如果 traces.txt 太大,先用 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 检测与处理流程 主线程阻塞 (卡在耗时操作) 输入事件派发超时 (5秒无响应) 前台广播超时 (10秒无响应) Service/CP 超时 (20s/10s) 系统判定 ANR (弹出对话框) 生成 traces.txt (/data/anr/下) 开发者分析日志 触发条件 (4类超时)

核心知识点:ANR 的本质是系统对主线程响应能力的一种「健康检查」。只要主线程在关键超时窗口内无法处理事件,就会被判定为 ANR,跟应用是否真正崩溃无关。很多开发者以为 ANR 是偶发 bug,其实只要我们能把耗时操作扔出主线程,ANR 就能从根源上避免。

避坑指南:我曾经在集成某个第三方的 Keystore 封装库时,它的 sign() 方法内部没有做异步处理,导致主线程直接调用了底层 C++ 的加密计算。虽然只有几十毫秒,但叠加了主线程原有的 UI 绘制和事件分发,最终就踩到了 5 秒的红线。所以——任何可能阻塞主线程的 I/O、计算、加解密操作,都一定要异步化。

怎样快速定位 ANR 根因?

  1. 看主线程堆栈 —— 搜索 "main" 线程,找到卡住的调用链。
  2. 看锁等待 —— 如果出现 waiting to lock,说明在等一把锁,顺着锁的地址找到哪个线程持有它。
  3. 看 CPU/IO —— traces.txt 头部有 CPU 负载、磁盘 IO 的统计,可以辅助判断是否系统资源不足导致主线程被饿死。

举个例子,有一次我查到一个 ANR 日志里主线程在 Thread.sleep(),sleep 了 8 秒。你想想看,哪个正经业务会在主线程睡 8 秒?原来是一个测试同事随手写了个调试代码没删掉。嗯,遇到这种奇葩情况,日志一看就懂了。