13、Logcat 与 ANR:如何通过日志定位应用无响应问题

ANR,全称 Application Not Responding,说白了就是应用没反应了。用户点屏幕没反馈,系统弹个对话框问「是否要关闭」。这玩意儿在线上是用户流失的元凶之一,我见过不少产品因为 ANR 率过高被应用商店下架。

今天我就带你从 Logcat 的角度,把 ANR 的定位方法彻底讲透。你想想看,系统其实已经帮你把线索都写在日志里了,关键是你知不知道怎么看。

13.1 ANR 的本质是什么

ANR 不是 bug,是系统的一种保护机制。当主线程被阻塞超过一定时间,系统就会判定它「无响应」。

触发条件其实就三种:

  • 输入事件:5 秒内没处理完触摸或按键事件
  • 广播接收器:onReceive 方法执行超过 10 秒
  • Service:前台服务 20 秒、后台服务 200 秒没执行完

嗯,这里要注意,ANR 的阈值在不同 Android 版本上略有差异,但大体框架没变过。

13.2 ANR 日志长什么样

当 ANR 发生时,系统会往 Logcat 里写一条关键日志。我习惯先搜 ANR in 这个关键字,一找一个准。

// 典型的 ANR 日志头
I/ActivityManager: ANR in com.example.myapp (com.example.myapp/.MainActivity)
  PID: 12345
  Reason: Input dispatching timed out (Waiting because no window has focus...)
  Load: 2.15 / 1.98 / 1.72
  CPU usage: 4500ms / 5000ms

这条日志告诉你三件事:哪个进程 ANR 了、原因是什么、CPU 负载情况。我个人经验是,先看 Reason 字段,它直接告诉你阻塞类型。

核心思路:ANR 日志 = 问题描述 + 堆栈信息 + CPU 状态。三者缺一不可。

13.3 堆栈信息才是破案关键

光看 ANR 头还不够,真正的线索藏在 traces.txt 里。系统会在 ANR 发生时自动 dump 所有线程的堆栈,并输出到 Logcat。

// 主线程堆栈示例
"main" prio=5 tid=1 Blocked
  at com.example.myapp.MainActivity.onClick(MainActivity.java:45)
  - waiting to lock <0x0c4d5e60> (a java.lang.Object)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:154)
  at android.app.ActivityThread.main(ActivityThread.java:6119)

看到 Blocked 状态了吗?这就是主线程被卡住的直接证据。后面跟着的 waiting to lock 说明它在等一把锁。我曾经遇到过一个案例,主线程等一个数据库锁等了 8 秒,就是因为子线程持锁后做了大量 I/O 操作。

我的习惯:看到 Blocked 状态,立刻去搜对应的锁地址(比如 0x0c4d5e60),找到持锁线程,问题就解决了一半。

13.4 CPU 使用率:别被表象骗了

很多人看到 ANR 就以为是 CPU 不够用。其实不一定。你看这条日志:

CPU usage: 4500ms / 5000ms
  100% TOTAL: 2.3% user + 1.1% kernel + 96.6% idle

CPU 空闲率 96.6%,说明系统根本不忙。那 ANR 怎么来的?大概率是死锁或者主线程在等某个资源。反过来,如果 CPU 使用率接近 100%,那可能是主线程在做大量计算,比如在 UI 线程解析 JSON。

避坑指南:我曾经被 CPU 使用率骗过一次。当时看到 CPU 占用 80%,以为是计算量太大,结果优化完算法 ANR 还在。后来仔细看堆栈才发现是死锁。所以记住:CPU 使用率只是辅助信息,堆栈才是铁证。

13.5 实战:从 Logcat 定位 ANR 的完整流程

我总结了一套四步法,你在线上排查时可以直接套用:

  1. 搜关键字:在 Logcat 里搜 ANR in,找到发生时间点
  2. 看 Reason:判断是输入事件超时、广播超时还是 Service 超时
  3. 读堆栈:找到主线程状态,是 Runnable、Blocked 还是 Sleeping
  4. 查持锁线程:如果是 Blocked,找到锁地址对应的持锁线程

下面这张图把整个流程串起来了:

ANR 定位四步法流程图 ① 搜 ANR in 找到发生时间点 ② 看 Reason 判断超时类型 ③ 读堆栈 主线程状态分析 ④ 查持锁线程 找到锁地址对应线程 主线程状态 Runnable / Blocked / Sleeping 核心原则 堆栈信息 > CPU 使用率 > 日志时间戳 Blocked 状态 → 查死锁 | Runnable 状态 → 查计算量 Sleeping 状态 → 查 wait/notify 机制

13.6 常见 ANR 场景与日志特征

场景 主线程状态 典型日志特征 常见原因
死锁 Blocked waiting to lock <0x...> 两个线程互相等待对方释放锁
耗时计算 Runnable at ... heavyMethod() 主线程做 JSON 解析、图片解码等
I/O 阻塞 Blocked at java.io.FileInputStream.read 主线程读文件或网络请求
Binder 调用 Sleeping at android.os.BinderProxy.transact 主线程调用系统服务超时

我印象最深的一次 ANR 排查,就是主线程在 onClick 里直接调了 BitmapFactory.decodeStream,加载一张 4K 图片。堆栈里清清楚楚写着 nativeDecodeStream,CPU 使用率飙到 90%。说白了,就是不该在主线程做的事,硬塞进去了。

13.7 线上 ANR 日志的获取技巧

如果你在开发阶段,直接连 Android Studio 看 Logcat 就行。但线上环境呢?用户手机不在你手里。这时候我建议你用以下方式:

  • adb bugreport:生成完整报告,里面包含 traces.txt
  • Firebase Crashlytics:自动采集 ANR 堆栈
  • 自定义 ANR 监控:通过 Application.registerActivityLifecycleCallbacks 配合 Handler 检测主线程卡顿

我的经验:线上 ANR 日志往往不全,因为用户可能不会立刻上传。我习惯在应用启动时主动检查本地是否有未上传的 traces 文件,这样能提高采集率。

13.8 小结

定位 ANR 其实没那么玄乎。记住三个关键字:ANR in 找进程,Blocked 找死锁,Runnable 找耗时。堆栈信息永远是最可靠的证据,CPU 使用率只是辅助判断。

下次遇到 ANR,别慌。打开 Logcat,按我上面说的四步法走一遍,问题大概率能水落石出。


公众号:蓝海资料掘金营,微信deep3321