第2章 ANR日志分析:解读traces.txt与logcat的关键信息
2.1 traces.txt:ANR的第一现场
当Android系统判定你的应用发生了ANR,它会立刻给进程发送一个SIGNAL,并触发一次快照——这就产生了traces.txt。我这些年排查ANR,每次第一件事就是去拿这份文件。它记录了ANR发生时所有线程的调用栈,说白了就是谁在跑、谁在等、谁卡住了。
小技巧:traces.txt的位置通常在/data/anr/traces.txt,如果是多用户设备,可能带个后缀。线上版本可以通过adb pull /data/anr/*.txt获取,或者用Bugly、Sentry这类平台直接拉取。
我们来看一段典型的traces.txt片段:
----- pid 12345 at 2025-02-21 14:30:00 -----
Cmd line: com.example.myapp
DALVIK THREADS (20):
"main" prio=5 tid=1 WAIT
| group="main" sCount=1 dsCount=0 flags=1 obj=0x1234
| sysTid=12345 nice=0 cgrp=default sched=0/0
| heldHolders=lockedFrom=stack=0
| state=waiting on <0x5678> (a java.lang.Object)
at java.lang.Object.wait(Native method)
at java.lang.Object.wait(Object.java:423)
at com.example.MyManager.getData(MyManager.java:45)
at com.example.MainActivity.onResume(MainActivity.java:80)
...
"Binder:12345_1" prio=5 tid=11 BLOCKED
| heldHolders=lockedFrom=stack=0
| waiting to lock <0x5678> (a java.lang.Object) held by "main"+tid=1
at com.example.MyManager.processData(MyManager.java:100)
...
关键信息就藏在里面。我一般会先看main线程的状态:WAIT、BLOCKED、RUNNABLE还是NATIVE。如果是WAIT,说明线程在等notify;如果是BLOCKED,那就是锁竞争。再看锁对象地址——比如这里的<0x5678>,它指向一个具体的锁实例。
2.2 logcat:ANR的前因后果
traces.txt是静态快照,logcat则记录了ANR发生前后的系统日志。我习惯用下面的命令过滤:
adb logcat -v time -b events | grep -E "am_anr|anr|InputDispatcher"
常见的ANR logcat关键输出长这样:
02-21 14:30:00.123 567 890 I am_anr : [0,12345,com.example.myapp,3148845538,Input dispatching timed out (Waiting because no window has focus)]
02-21 14:30:00.456 567 890 W ActivityManager: ANR in com.example.myapp (com.example.myapp/.MainActivity)
PID: 12345
Reason: Input dispatching timed out (Waiting because no window has focus)
Load: 6.15 / 4.32 / 3.78
CPU usage from 12345ms to 0ms ago:
73% 12345/com.example.myapp: 63% user + 10% kernel
9% 890/system_server: 5% user + 4% kernel
...
100% TOTAL: 73% user + 27% kernel
这里有个细节:Load和CPU usage能帮你判断是不是系统整体负载过高。如果进程CPU占比不高(比如低于30%),那大概率是线程卡在锁或wait上面。如果CPU占比飙到90%以上,那可能是主线程在做大量计算。
2.3 常见ANR模式与特征对照
| 日志模式 | traces.txt典型表现 | logcat关键字段 | 常见根因 |
|---|---|---|---|
| 输入分发超时 | main线程 WAIT/BLOCKED,堆栈停在View或Input相关方法 | Input dispatching timed out | 主线程被锁或耗时操作阻塞 |
| 广播超时 | main线程在BroadcastReceiver.onReceive中 | Broadcast of Intent | 主线程执行网络/IO/DB操作 |
| 服务超时 | main线程在Service.onCreate/onStartCommand中 | Executing service | 服务在主线程中做耗时任务 |
| ContentProvider超时 | main线程在Provider.query中 | ContentProvider not responding | Provider操作超时,可能涉及锁 |
举个例子,我之前排查过一个问题:logcat里显示Input dispatching timed out,traces.txt里main线程是WAIT状态,堆栈停在Object.wait(),而Binder_1线程是BLOCKED状态,等同一把锁。很明显,这是锁竞争导致——一个线程拿着锁不放手,主线程只能等。后来发现是后台线程忘了释放一个同步块,典型的内存泄漏(其实是对象生命周期管理问题)。
2.4 快速定位:从日志到代码
我总结了一套判断逻辑,你可以直接在脑子里过一遍:
- 看main线程状态:RUNNABLE → 计算型ANR;WAIT/BLOCKED → 锁或等待型。
- 看锁对象:在traces.txt中搜索同一地址的锁,看谁持有它,谁在等它。
- 看logcat的cpu usage:进程CPU% > 50% 且主线程RUNNABLE → 算法或循环问题;CPU% < 30% 且主线程WAIT → 锁竞争或死锁。
- 看线程数量:如果线程池创建了上百个线程,可能就是对象泄漏导致线程不释放。
我曾经犯过的错:有一次traces.txt里main线程显示为NATIVE状态,我以为是系统调用阻塞,结果忽略了堆栈最底层是epollWait——其实是主线程在等Handler消息。后来才反应过来:NATIVE状态不一定是死锁,也可能是空闲。一定要结合堆栈内容判断。
2.5 本章知识体系图
下面这张图概括了ANR日志分析的完整路径。你可以把它当成排查时的检查清单:
2.6 实战案例:从traces.txt揪出对象泄漏
有一次线上ANR率飙到0.5%,logcat里全是Input dispatching timed out。traces.txt里main线程在等一个RecyclerView的动画回调锁,而另一个名字是animator-pool-1的线程一直是WAIT状态。奇怪的是这个线程持有一把锁,堆栈停在Animator.finalize()——没错,这就是个典型的对象泄漏。
为什么?因为Animator对象被某个回调持有,但对应的View已经被移除,导致finalize迟迟不被GC调用(因为还有强引用),而finalize里又用了同步块,卡死了其他等待这个锁的线程。最终我们发现是一个内部类持有Activity引用,形成了闭包泄漏。修复后ANR率直接降到0.02%。
核心结论:traces.txt里的线程名和栈帧往往能直接暴露泄漏路径。如果发现一个线程长时间停留在finalize()或NativeDestroy之类的方法里,并且持有关键锁,优先检查对应对象的生命周期管理。