9、日志高级技巧:logcat 按标签过滤、logcat 按 PID 过滤、清空日志缓冲区、实战:分析 ANR 日志
日志这东西,说白了就是系统的「黑匣子」。我刚入行那会儿,觉得看日志就是瞎猫碰死耗子,满屏滚过去,眼睛都看花了。后来才明白,不是日志没用,是你不会用。今天咱们就把 logcat 的高级玩法掰开揉碎讲清楚。
9.1 为什么需要高级过滤?
你想想看,一个正在运行的 Android 系统,每秒能产生几百条日志。系统服务、应用进程、内核驱动……全混在一起。你要是直接 adb logcat,那画面简直不忍直视。
我记得有一次线上出了个 bug,用户反馈 App 闪退。我连上设备,日志刷得跟瀑布似的,根本找不到关键信息。后来用了标签过滤,十秒钟就定位到了问题。嗯,从那以后,我再也不裸看日志了。
9.2 按标签过滤(Tag Filter)
每个日志条目都有个标签(Tag),通常代表模块名或类名。比如 ActivityManager、SystemServer、MyApp 等等。
基本语法很简单:
adb logcat -s TagName
举个例子,我只想看 ActivityManager 的日志:
adb logcat -s ActivityManager
这会把其他所有标签都屏蔽掉,只显示 ActivityManager 的内容。干净利落。
如果你想看多个标签,用逗号分隔:
adb logcat -s ActivityManager,WindowManager
还有一种更灵活的写法——用正则表达式过滤:
adb logcat -s "MyApp.*"
这会匹配所有以 "MyApp" 开头的标签。我在项目中遇到过,团队里不同人写的 Tag 风格不统一,有的用下划线,有的用驼峰。用正则一把梭,省心多了。
9.3 按 PID 过滤(Process ID Filter)
有时候,同一个标签可能被多个进程使用。比如 "SystemServer" 这个标签,系统服务里一堆线程都在用。这时候按标签过滤就不够精确了。
你需要的是按进程 ID 过滤。
先找到目标进程的 PID:
adb shell ps | grep com.example.myapp
假设返回的 PID 是 12345,那么:
adb logcat --pid=12345
这就只显示该进程的日志了。其他进程的日志一概不显示。
我常用的组合拳是这样的:
# 先获取 PID
PID=$(adb shell pidof com.example.myapp)
# 再过滤日志
adb logcat --pid=$PID -v threadtime
为什么要加 -v threadtime?因为这样能看到线程 ID 和时间戳,方便分析并发问题。
9.4 清空日志缓冲区
日志缓冲区是有大小限制的。默认情况下,主缓冲区是 256KB,事件缓冲区是 256KB,崩溃缓冲区是 128KB。满了之后,新的日志会覆盖旧的。
所以,在开始一轮新的调试之前,我建议先清空缓冲区:
adb logcat -c
这个命令会清空所有日志缓冲区。之后再执行 adb logcat,你看到的就是从这一刻开始的新日志。
为什么要这么做?
- 避免旧日志干扰视线
- 方便复现问题后直接抓取关键日志
- 减少缓冲区溢出导致日志丢失的风险
如果你只想清空某个特定的缓冲区,可以指定 -b 参数:
adb logcat -b main -c # 只清空主缓冲区
adb logcat -b events -c # 只清空事件缓冲区
9.5 实战:分析 ANR 日志
ANR(Application Not Responding)是 Android 开发者最头疼的问题之一。说白了,就是你的应用在主线阻塞了太久,系统看不下去了,直接弹个对话框告诉用户「这个应用无响应」。
ANR 的日志分析,核心就三步:
- 找到 ANR 发生的进程
- 查看主线程的堆栈
- 分析阻塞原因
9.5.1 抓取 ANR 日志
当 ANR 发生时,系统会把相关信息写入 /data/anr/traces.txt。你可以用 adb 把它拉下来:
adb pull /data/anr/traces.txt
但更直接的方式是,在 logcat 里实时观察 ANR 事件:
adb logcat -b events | grep "am_anr"
这会过滤出事件缓冲区中所有与 ANR 相关的日志。输出大概长这样:
I am_anr : [0,12345,com.example.myapp,1234567890,Input dispatching timed out]
其中 12345 就是发生 ANR 的进程 PID。
9.5.2 分析主线程堆栈
拿到 PID 之后,用 --pid 过滤出该进程的所有日志:
adb logcat --pid=12345 -v threadtime
重点关注主线程(通常线程 ID 为 1)的日志。你会看到类似这样的堆栈信息:
2025-01-15 14:30:45.123 12345 12345 I System.out:
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x...
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x...
| state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
| stack=0x7f8c400000-0x7f8c402000 stackSize=8192KB
| held by tid=5 (AsyncTask #1)
at com.example.myapp.MainActivity.onClick(MainActivity.java:45)
- waiting to lock <0x...> (a java.lang.Object)
at android.view.View.performClick(View.java:7444)
...
看到 tid=1 Blocked 了吗?这就是主线程被阻塞了。后面跟着的 waiting to lock 说明它在等一把锁,而锁被 tid=5 的 AsyncTask 持有着。
tid=1:主线程Blocked:线程状态为阻塞waiting to lock:在等待锁释放held by tid=5:锁被线程 5 持有
9.5.3 定位根因
找到锁的持有者之后,继续看线程 5 的堆栈:
2025-01-15 14:30:45.124 12345 12349 I System.out:
"AsyncTask #1" prio=5 tid=5 Sleeping
| group="main" sCount=1 dsCount=0 flags=1 obj=0x...
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x...
| state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
| stack=0x7f8c300000-0x7f8c302000 stackSize=8192KB
| held by tid=1 (main)
at java.lang.Thread.sleep(Native Method)
at com.example.myapp.DataLoader.loadData(DataLoader.java:30)
...
线程 5 在 sleep,而且它持有了主线程需要的锁。这就形成了死锁——主线程等线程 5 释放锁,线程 5 在 sleep 不干活。
9.6 知识体系总览
下面这张图总结了本章的核心知识点,方便你快速回顾:
9.7 总结
日志分析是 Android 调试的基本功。掌握了按标签过滤、按 PID 过滤、清空缓冲区这三个技巧,你就能在茫茫日志海中快速定位目标。而 ANR 分析,说白了就是找到主线程被谁堵住了,然后解决它。
我个人习惯把常用命令写成脚本,比如 logcat-myapp.sh,一键过滤、清空、输出到文件。调试效率能提升一大截。
记住一句话:日志不会骗人,但前提是你会看。
公众号:蓝海资料掘金营,微信deep3321