日志抓取与 logcat 高级过滤技巧
日志是调试 SystemUI 崩溃的“第一手证据”,但光会 adb logcat 远远不够。你想想看,几百行日志里可能只有三五条是真正的线索。怎么快速锁定目标?怎么保证抓到的日志不丢帧?这章我就把压箱底的经验抖出来。
抓日志,别只会敲 adb logcat
很多新人上来就一句 adb logcat,然后屏幕刷刷刷刷……看完眼都花了。我个人习惯先把日志重定向到文件:
adb logcat -v threadtime > /tmp/crash.log
为什么要加 -v threadtime?因为 默认格式没有毫秒和线程 ID,一旦崩溃涉及多线程交互,你根本没法对齐事件顺序。我记得有次排查 SystemUI 的 ANR,就是靠 threadtime 格式才看出某个 binder 线程卡了 3 秒。
还有个小细节:抓日志前建议先把旧日志清一下,避免干扰:
adb logcat -c
不过注意,-c 只清 main 缓冲区。如果你想连 system 和 crash 缓冲区一起清,要指定 -b all。
adb logcat -c,这会丢失用户之前的崩溃日志。我一般在开发机上才这么干。
缓冲区选不对,等于白抓
logcat 有多个缓冲区,默认只抓 main 和 system。但 SystemUI 崩溃很多时候会出现在 crash 或 events 缓冲区里。你看下面这张图就清楚了:
所以,抓 SystemUI 崩溃的正确姿势是:
adb logcat -b crash -b system -v threadtime > crash.log
缓冲区可以同时抓多个,用 -b 连续指定。我之前踩过坑——只抓了 main 缓冲区,结果 logcat 里全是应用日志,崩溃堆栈被刷没了。后来改成 -b crash -b system,问题一次就定位了。
logcat 高级过滤,三板斧
第一板斧:按等级压噪
SystemUI 正常的调试日志很多,但崩溃通常只在 Error / Fatal 级别出现。我一般直接这样:
adb logcat -b crash -v threadtime *:E
这会把所有低于 Error 的日志都去掉。但要注意:有些关键警告是 W 级别的(比如内存不足的前兆),所以我的习惯是 先抓 *:W,如果量还是太大就缩到 *:E。
第二板斧:按标签锁定模块
SystemUI 的日志标签大多以 SystemUI 开头,也可能是具体的类名:
adb logcat -b crash -v threadtime SystemUI:E
如果想同时看多个标签,用逗号隔开:
adb logcat -b crash SystemUI:W StatusBar:W NavigationBar:W
这里我有个小建议:不要只过滤 Error,有时候 前几行 Warning 才是真正的根因,Error 只是后果。我在项目中遇到过,一次 SystemUI 重启的堆栈指向是 NullPointer,但真正的原因在 2 秒前有一条 “KeyguardViewMediator: Trying to show keyguard while not allowed!” 的 Warning。
第三板斧:正则与 PID 精确打击
有时崩溃日志不包含标准标签,比如 native crash 会出现在 crash 缓冲区,但 tag 是 DEBUG。这时候正则就派上用场了:
adb logcat -b crash -v threadtime -e "signal|fatal|SIGSEGV"
-e 后面跟正则表达式,大小写敏感。另一个技巧是抓特定进程:先找到 SystemUI 的 PID:
adb shell pidof com.android.systemui
然后直接按 PID 过滤:
adb logcat -b crash -v threadtime --pid 1234
这样输出里除了 SystemUI 自己的日志,还包含它调用的系统服务日志?不,--pid 只打印该进程自己打印的日志,对跨进程 binder 调用要小心。我建议 先按 PID 抓,再配合 -b crash 看有没有跨进程的异常。
| 目标 | 命令 |
|---|---|
| 抓 crash + system 缓冲区,带时间戳 | adb logcat -b crash -b system -v threadtime |
| 只显示 Error 及以上 | *:E |
| 只看 SystemUI 的 Error | SystemUI:E |
| 正则匹配崩溃关键词 | -e "NullPointer|RuntimeException" |
| 按进程 PID 过滤 | --pid 1234 |
| 实时保存到文件并显示 | adb logcat > log.txt & |
实时过滤与日志旋转
崩溃往往在几秒内发生,手动盯着 logcat 太累。我在调试时常用 grep 做管道过滤:
adb logcat -b crash -v threadtime | grep -i "systemui\|fatal"
但要注意:管道后的 grep 默认行缓冲,速度跟不上时可能会丢日志。更靠谱的做法是用 python 脚本,像这样:
import subprocess
import sys
proc = subprocess.Popen(['adb', 'logcat', '-b', 'crash', '-v', 'threadtime'],
stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
for line in proc.stdout:
if 'SystemUI' in line and 'E/' in line:
sys.stdout.write(line)
sys.stdout.flush()
这段脚本我一直在用,bufsize=1 保证行缓冲,不会漏日志。
还有一个痛点:日志文件越来越大。我个人习惯加 --rotate-kbytes 参数(Android 10+ 支持):
adb logcat -b crash -v threadtime --rotate-kbytes 1024 -f /sdcard/crash.log
每 1MB 自动滚动一个新文件,文件名自动追加序号。不过要注意 这个参数只在 device 端的 logcat 生效,PC 端 adb logcat 的 -f 是指向手机文件。我建议还是直接重定向到电脑,更可靠。
adb shell dumpsys battery unplug 模拟意外断电?不用,但可以 adb shell settings put global sysui_tracing_default_enabled 1 开启 SystemUI 的内部 trace,这样即使 logcat 没及时抓到,trace 文件里也有线索。
避坑指南——我曾经犯过的错
- 坑 1:日志被截断——我遇到过 SystemUI 崩溃堆栈特别长,默认 logcat 单行 4096 字符被截断。后来加
-v long或--print-long才看到完整堆栈。 - 坑 2:时间戳不准——不同设备的时钟不同步,如果你同时用 PC 和 device 的日志,一定要用
-v epoch转成 UNIX 时间戳再比对。我曾因为差了 3 秒,误判了崩溃顺序。 - 坑 3:抓了天量日志——有一次忘记加等级过滤,抓了 2GB 文本,打开就卡死。现在我的习惯:先加
*:W,觉得线索不足再降级到*:V。 - 坑 4:权限不够——某些系统日志(如 kernel log)需要 root 或工程机。如果
adb logcat提示Permission denied,试试adb root或换用logcat -b kernel -b crash的拆分。
adb logcat,否则 logd 服务会积压大量待消费日志,导致系统变慢甚至触发 watchdog。我一般只抓 30 秒内复现的场景,够用即可。
小结:调试第一关
日志抓取是调试的“脏活”,但脏活做好了,后续分析就事半功倍。记住三个要点:
选对缓冲区 → 压噪到适当的等级 → 用标签或正则缩小范围。
这章讲的是“怎么抓”,下一层咱们再聊“怎么读”——堆栈解析、时序重建,那一部分更有意思。嗯,就先到这,去试试我上面那些命令,拿个模拟崩溃验证一下,你会发现自己突然就“会抓日志”了。