日志系统与抓取方法:logcat、kernel log、tombstone、anr trace、dmesg的抓取与分析

做系统稳定性,说白了就是跟各种日志打交道。我经常跟团队里的小朋友说,日志就是系统的「黑匣子」,飞机失事要找黑匣子,手机死机要找日志。今天咱们就把这几种核心日志的抓取和分析方法,一次性讲透。

一、logcat:最常用的用户态日志

logcat 是 Android 最基础的日志工具。它抓的是用户空间(用户态)的日志,包括 App、Framework、系统服务等打印的信息。

1. 基本抓取命令

// 抓取所有日志,保存到文件
adb logcat -v threadtime > logcat.txt

// 按缓冲区抓取
adb logcat -b main -b system -b crash -v threadtime > logcat.txt

// 按 TAG 过滤
adb logcat -s ActivityManager:* WindowManager:* -v threadtime

我个人习惯用 -v threadtime 参数,它会把线程 ID 和时间戳都带上。排查问题时,时间顺序和线程归属太重要了。

小技巧: 抓取 ANR 相关日志时,建议同时抓 main、system、crash 三个缓冲区。我遇到过好几次,ANR 的真正原因藏在 system 缓冲区里,main 缓冲区反而看不出问题。

2. 日志级别与过滤

级别 含义 使用场景
V Verbose(明细) 开发调试,正式环境一般不抓
D Debug(调试) 开发阶段排查问题
I Info(信息) 正常流程记录
W Warning(警告) 可能有问题,需要关注
E Error(错误) 必须修复的问题
F Fatal(致命) 会导致进程崩溃

嗯,这里要注意:很多同学一上来就抓 V 级别的日志,结果文件巨大,反而找不到关键信息。我建议先抓 E 和 W 级别,定位到具体模块后再放大级别。

二、kernel log:系统底层的「心电图」

Kernel log 记录的是内核空间的日志。当系统出现死机、重启、驱动异常时,logcat 可能已经来不及打印了,但 kernel log 往往还留着关键线索。

1. 抓取方法

// 通过 adb 抓取
adb shell dmesg > kernel_log.txt

// 实时查看
adb shell cat /proc/kmsg   // 需要 root 权限

// 或者用 logcat 抓 kernel 缓冲区
adb logcat -b kernel -v threadtime > kernel_logcat.txt

我曾经遇到一个案子,手机不定时重启,logcat 里啥都没有。后来抓了 dmesg,发现是某个 GPIO 口的中断风暴导致内核 panic。你看,这种问题不到 kernel 层根本查不出来。

注意: /proc/kmsg 只能被一个进程打开。如果你已经开了一个终端在读,另一个终端就打不开了。这时候用 dmesg 命令更稳妥。

2. 关键信息解读

Kernel log 里最需要关注的是这几个关键词:

  • panic:内核恐慌,系统会直接重启
  • Oops:内核出错了,但不一定重启
  • watchdog:看门狗超时,说明某个任务卡住了
  • Out of memory:内存不足,开始杀进程
  • Call trace:函数调用栈,告诉你代码走到哪崩了

三、tombstone:App 崩溃的「尸检报告」

Tombstone 是 native 层(C/C++)崩溃时生成的日志文件。Java 层的崩溃会记录在 logcat 里,但 native 层的崩溃,比如 so 库挂掉、JNI 调用出错,就会生成 tombstone。

1. 抓取位置

// tombstone 文件默认路径
/data/tombstones/

// 拉取到电脑
adb pull /data/tombstones/ ./tombstones/

// 查看最近一次崩溃
adb shell ls -lt /data/tombstones/

每个 tombstone 文件以 tombstone_XX 命名,XX 是数字编号。最新的崩溃会覆盖最旧的文件,所以出问题后要尽快拉取。

2. 分析要点

打开一个 tombstone 文件,你会看到类似这样的结构:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'xxx'
Revision: '0'
ABI: 'arm64'
Timestamp: 2024-01-15 14:30:22.123456789+0800
pid: 12345, tid: 12346, name: Binder:12345_1
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
    x0  0000000000000000  x1  0000007f8c123456  ...
    pc  0000007f8a123456  /system/lib64/libxxx.so
backtrace:
    #00 pc 0000000000123456  /system/lib64/libxxx.so (function_name+100)
    #01 pc 0000000000123789  /system/lib64/libxxx.so (another_func+200)
    #02 pc 0000000000123901  /system/lib64/libxxx.so

核心看三点:

  • signal 和 fault addr:SIGSEGV 是段错误,fault addr 为 0 说明是空指针
  • pc 寄存器:程序计数器,指向崩溃时的指令地址
  • backtrace:调用栈,从下往上看是调用顺序
实战经验: 我见过最坑的一次,tombstone 里 backtrace 全是问号。后来发现是 so 库被 strip 掉了符号表。所以发布版本一定要保留符号表文件(.sym 文件),不然 tombstone 基本没法分析。

四、ANR Trace:应用无响应的「现场快照」

ANR(Application Not Responding)发生时,系统会生成一份 trace 文件,记录所有线程的当前状态。说白了,就是系统在那一刻给所有线程拍了一张「合影」。

1. 抓取方法

// ANR trace 文件路径
/data/anr/traces.txt

// 拉取到电脑
adb pull /data/anr/traces.txt ./

// 如果 ANR 刚发生,也可以直接看
adb shell cat /data/anr/traces.txt | grep "ANR in"

注意,traces.txt 是覆盖写入的。每次 ANR 都会覆盖上一次的内容。所以如果你遇到连续 ANR,只能抓到最后一次的 trace。

2. 分析主线程

ANR 的核心是主线程被阻塞了。打开 traces.txt,先找到主线程(通常叫 "main" 或 "UI Thread"):

"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=Blocked stack=0x7f8c000000-0x7f8c002000
  at com.example.MyClass.myMethod(MyClass.java:100)
  - waiting to lock <0x12345678> (a java.lang.Object)
  at com.example.MyClass.anotherMethod(MyClass.java:50)
  - locked <0x12345678> (a java.lang.Object)
  at android.app.ActivityThread.handleMessage(ActivityThread.java:2000)

看到 Blocked 状态了吗?说明主线程在等一把锁。这时候要找到谁持有了这把锁。继续往下翻,找到持有锁的线程:

"Binder:12345_2" prio=5 tid=15 Sleeping
  at java.lang.Thread.sleep(Native Method)
  - locked <0x12345678> (a java.lang.Object)

你看,Binder 线程持有了锁,但它自己在 sleep。主线程只能干等着。这就是典型的死锁或锁竞争问题。

避坑指南: 我曾经遇到一个案子,traces.txt 里主线程显示的是 Runnable 状态,但 ANR 还是发生了。后来发现是主线程在做大量的 Binder 调用,虽然没阻塞,但耗时太长。所以 ANR 不一定是死锁,也可能是主线程太忙了。

五、dmesg:内核日志的「实时快照」

其实 dmesg 和 kernel log 是一回事。dmesg 是 Linux 标准的命令,用来读取内核环形缓冲区的内容。Android 里也完全兼容。

1. 常用参数

// 查看所有日志
dmesg

// 只看最后 100 行
dmesg | tail -100

// 带时间戳
dmesg -T

// 实时监控
dmesg -w

-T 参数会把内核时间戳转换成人类可读的时间格式。我建议每次都加上,不然看到的是系统启动后的秒数,换算起来太痛苦了。

2. 实战场景

什么时候必须看 dmesg?我总结了几种情况:

  • 系统重启:看最后几行有没有 panic 或 watchdog 信息
  • 外设异常:比如摄像头打不开、WiFi 连不上,dmesg 里通常有驱动报错
  • 功耗问题:看是否有设备没有进入休眠,比如 "wake lock" 相关的日志
  • 存储问题:I/O 错误、文件系统损坏等

六、综合抓取策略

实际项目中,我一般会同时抓取多种日志。因为单一日志往往只能看到问题的一个侧面。

1. 一键抓取脚本

#!/bin/bash
# 抓取所有关键日志
timestamp=$(date +%Y%m%d_%H%M%S)
mkdir -p bugreport_$timestamp

# 1. logcat
adb logcat -b main -b system -b crash -v threadtime -d > bugreport_$timestamp/logcat.txt

# 2. kernel log
adb shell dmesg > bugreport_$timestamp/dmesg.txt

# 3. tombstone
adb pull /data/tombstones/ bugreport_$timestamp/tombstones/

# 4. ANR trace
adb pull /data/anr/traces.txt bugreport_$timestamp/

# 5. 系统信息
adb shell cat /proc/meminfo > bugreport_$timestamp/meminfo.txt
adb shell cat /proc/cpuinfo > bugreport_$timestamp/cpuinfo.txt

echo "日志已保存到 bugreport_$timestamp"

2. 分析顺序

拿到一堆日志后,我习惯按这个顺序排查:

  1. 先看 dmesg:确认内核有没有 panic 或硬件异常
  2. 再看 tombstone:有没有 native 层崩溃
  3. 然后看 ANR trace:主线程是否被阻塞
  4. 最后看 logcat:结合时间戳,看崩溃前后的用户态日志
重要提醒: 日志抓取要趁早!系统重启后,dmesg 和 logcat 的缓冲区会被清空。tombstone 和 ANR trace 虽然还在,但也会被后续的崩溃覆盖。我见过太多人,手机重启了才想起来抓日志,结果啥都没抓到。

好了,日志系统这块就讲这么多。说白了,抓日志是体力活,分析日志才是技术活。多练、多积累,你也能从一堆乱码中一眼看出问题所在。下次咱们聊聊具体怎么分析这些日志,比如怎么从 backtrace 定位到代码行号,怎么用 addr2line 解析符号表。到时候见。