Skip to content

ADB logcat进阶

追踪 adb logcat 到 logd/liblog 的读取链路,解释 buffer、过滤、格式、历史窗口、实时跟随、统计、清空和文件轮转的真实边界。

基于android-17.0.0_r1
AndroidADBlogcatlogdliblog日志诊断

ADB logcat进阶 ​

很多人把 adb logcat 记成“把设备日志打印到终端”。这个说法能帮助你第一次运行命令,却不足以解释真正的行为:日志由进程写入 logd 的多个 ring buffer,logcat 在设备端通过 liblog reader 打开这些 buffer,再负责过滤、格式化、计数和输出;host 端的 adb 主要只是把命令路由到设备 shell。

因此,下面几类问题属于不同 owner:

  • 为什么命令能启动但看不到某个 buffer:buffer 选择、设备编译变体或权限;
  • 为什么 -t 结束而 -T 继续等待:reader 的阻塞模式;
  • 为什么 *:S MyTag:D 与 ANDROID_LOG_TAGS 结果不同:过滤规则来源和优先级;
  • 为什么 -G 改了大小却没有让旧日志变多:ring buffer 容量与已有记录不是同一状态;
  • 为什么 -f 的文件会轮转:logcat 自己的输出文件状态,不是 logd ring buffer 轮转。

前置阅读是 ADB架构 与 ADB shell机制。本文从 packages/modules/adb 和 system/logging 的真实入口解释 logcat 读取和诊断边界,不展开应用 Log API、logd 完整写入路径或 bugreport 编排。需要把日志放入跨服务快照时,可继续阅读 bugreport分析。

本文面向已经会运行基本 adb logcat、但还不能解释 buffer、reader、filter 和 output writer 分工的 读者。读完后,应能从 host 命令追到设备端 Logcat::Run 和 liblog reader,判断空输出、EOF、轮转 失败与权限拒绝分别属于哪一层,并用对照实验验证 -t、-T、FILTERSPEC 和文件轮转的真实语义。

1. Logcat 所有权 ​

owner真实入口拥有的状态消费者
host ADBclient/commandline.cpp::logcatargv、ANDROID_LOG_TAGS 拼接设备 shell service
logcatlogcat.cpp::Logcat::Runbuffer mask、format、tail、regex、输出文件liblog reader 和 stdout/file
libloglogger_read.cpplogger list、每个 log id 的 fd、读取模式logd socket
logdLogBuffer 等ring buffer、大小、prune、读者连接logcat 与日志写入者

ADB 不是日志的持久化 owner。adb logcat 退出后,logd 仍然持有 buffer;adb logcat -c 发送的是清空请求;adb logcat -f file 则由 logcat 进程在设备端打开文件并写入。

2. Host路由 ​

2.1 Host路径 ​

Android 17 的 client/commandline.cpp::logcat() 读取 host 环境变量 ANDROID_LOG_TAGS,先用 escape_arg() 保护其内容,再构造:

text
export ANDROID_LOG_TAGS='<host value>'; exec logcat <arguments>

然后调用已有的 shell command 路径。adb longcat 只是额外追加 -v long,并没有另一套日志读取协议。真正的 getopt_long()、buffer 打开和记录读取发生在设备上的 /system/bin/logcat。

2.2 消费者 ​

adb logcat -s MyTag:D 的 -s、MyTag:D 都由设备 logcat 消费;host 只负责把参数作为 shell command 的一部分传过去。相反,host 的 shell 终端模式、PTY/raw 和 shell protocol 仍由 ADB shell机制 所述的 ADB shell 路径决定。

这解释了一个常见误会:adb logcat 输出混乱时,先修改 adb shell 的 PTY 选项通常没有意义;应该检查 logcat format、buffer 和过滤器。

3. buffer 选择 ​

3.1 Buffer选择 ​

logcat.cpp 维护 id_mask,每个 -b 都会把一个 log_id_t 加入 mask。帮助文本列出:

buffer常见内容选择边界
main普通应用和 framework 主日志默认集合之一
systemsystem/server 相关日志默认集合之一
crash崩溃相关日志默认集合之一
kernel内核日志桥接userdebug/eng 等产品条件
radiotelephony/radio需显式选择,产品可能不可用
eventsevent tag 二进制日志读取时需要 tag map 解码
security安全日志Device Owner 或相应权限条件
stats统计相关日志设备权限和实现条件相关

没有任何 -b 时,Android 17 源码将 mask 设为 main,system,crash,kernel。-b default 选择前三个,不包含 kernel;-b all 把 mask 设为全部 bit。多个 -b 或逗号分隔列表会交给同一个解析分支。

bash
adb logcat -b main -b system
adb logcat -b main,events -d
adb logcat -b all -t 100

-b all 不是“保证能读所有 buffer”。它只是请求打开所有合法 id;android_logger_open() 失败的 buffer 会进入 open_device_failures,最终由 logcat 报错退出。

3.2 buffer 选择入口 ​

liblog 为每个选中的 log_id 打开 logger,但 logcat 对多个 buffer 的输出是交错的。-D 可以在 buffer 之间打印分隔线,帮助判断一条记录来自哪个 buffer;没有 -D 时,默认格式中的 metadata 不等于 buffer 名称。

源码文件:system/logging/logcat/logcat.cpp

相关函数/类型:Logcat::Run

cpp
std::unique_ptr<logger_list, decltype(&android_logger_list_free)> logger_list{
        nullptr, &android_logger_list_free};
if (tail_time != log_time::EPOCH) {
    logger_list.reset(android_logger_list_alloc_time(mode, tail_time, pid));
} else {
    logger_list.reset(android_logger_list_alloc(mode, tail_lines, pid));
}

for (int i = LOG_ID_MIN; i < LOG_ID_MAX; ++i) {
    if (!(id_mask & (1 << i))) continue;
    const char* buffer_name = android_log_id_to_name(static_cast<log_id_t>(i));
    auto logger = android_logger_open(logger_list.get(), static_cast<log_id_t>(i));
    if (logger == nullptr) {
        ReportErrorName(buffer_name, security_buffer_selected, &open_device_failures);
        continue;
    }
    if (clearLog && android_logger_clear(logger)) {
        ReportErrorName(buffer_name, security_buffer_selected, &clear_failures);
    }
}

id_mask 决定哪些 buffer 会创建 logger;因此 -b all 是一组打开请求,不是对每个 buffer 成功的保证。 同一循环还承载 -c 清空操作,说明清空是对已打开的 logd logger 发送控制请求,而不是删除 logcat 进程 自己的输出。

3.3 binary buffer ​

events、stats 和 security 记录可能是 binary。Logcat::ProcessBuffer() 对这些 id 打开 event tag map,通过 android_log_processBinaryLogBuffer() 生成可打印的 AndroidLogEntry;普通 main/system/crash 记录走 android_log_processLogBuffer()。

因此 -v descriptive 只对能够找到 event tag 描述的 binary event 有意义;找不到 tag 或 payload 不匹配时,不能把输出当成稳定的字段 API。

4. 过滤器 ​

4.1 过滤输出 ​

过滤规则形如:

text
<tag>[:priority]

优先级从 V、D、I、W、E、F 到 S。例如:

bash
adb logcat '*:S' MyTag:D
adb logcat ActivityManager:I '*:S'

*:S 先把默认规则设为 silent,再为指定 tag 加例外。若命令行没有 filterspec,设备 logcat 才会读取 ANDROID_LOG_TAGS;host 的 adb logcat helper 正是把 host 环境变量导出到设备命令里。

4.2 Run ​

Android 17 的 Run() 先处理强制 filters;否则若 argv 中还有 FILTERSPEC,使用命令行;只有没有命令行 FILTERSPEC 时,才读取设备进程环境中的 ANDROID_LOG_TAGS。非法表达式会在读取日志前直接报错。

4.3 --regex过滤 ​

--regex 在 ProcessBuffer() 之后匹配格式化前的 AndroidLogEntry 文本。它适合按消息内容筛选,但不改变 logd reader 已经打开的 buffer,也不能替代 tag priority。--max-count 限制打印行数;--print 只有与 regex 和 max-count 同时使用时才有特殊意义,会继续打印非匹配消息直到达到匹配计数。

bash
adb logcat --pid=1234 --regex='timeout|ANR' --max-count=20
adb logcat '*:S' MyTag:V --pid=1234

--pid 在 reader allocation 时传入 pid,属于读取请求条件;--uid 则在读到 log_msg 后由 logcat 检查 UID,并且读取其他用户日志需要 root/log/system 等权限。两者不能简单互换。

5. 输出格式 ​

5.1 输出状态 ​

-v threadtime 是默认格式,输出日期、时间、priority、tag、PID 和 TID。其他常用主格式包括:

格式主要字段
briefpriority、tag、PID
long完整 metadata,并用空行分隔消息
processPID
raw原始消息文本
tagpriority 与 tag
threadpriority、PID、TID
time日期、时间、priority、tag、PID
threadtime日期、时间、priority、tag、PID、TID

adverb 可以组合:color、descriptive、epoch、monotonic、printable、uid、usec、UTC、year。例如:

bash
adb logcat -v threadtime,usec,UTC
adb logcat -v color,brief
adb logcat -v long,year

格式改变的是 logcat 输出,不会改变 logd 中已经保存的 record。-v raw 也不是 binary dump,它仍然经过 logcat 的文本记录处理。

5.2 binary ​

-B 选择 BINARY,logcat 直接写出 log_msg 的原始记录;--proto 选择 protobuf 输出,每条消息前带有 8 字节 little-endian 长度。两者都不适合直接给人阅读,但适合工具消费;它们的字段兼容性要以 Android 17 的 logcat.proto 和 log_msg 定义为准。

6. 历史窗口 ​

6.1 -d ​

命令reader 模式语义
adb logcatblocking读取历史后继续等待新记录
adb logcat -dnon-blockingdump 当前可读记录后退出
adb logcat -t 100non-blocking取最近 100 行并退出
adb logcat -T 100blocking从最近 100 行位置开始,并继续跟随
adb logcat -t 'MM-DD hh:mm:ss.mmm'non-blocking输出该时间之后的历史记录并退出
adb logcat -T 'MM-DD hh:mm:ss.mmm'blocking从时间点开始读取并继续等待

源码中 -t 先设置 got_t 和 ANDROID_LOG_NONBLOCK,随后与 -T 共用时间/行数解析;纯数字被当作 tail 行数,其他字符串进入 parseTime()。-t 因此天然 dump,而 -T 不会因为使用了 tail 就自动退出。

6.2 log_msg ​

android_logger_list_read() 返回正数时,logcat 处理一个 log_msg。非阻塞模式遇到 EAGAIN、EWOULDBLOCK 或 ETIMEDOUT 会结束当前读取;阻塞模式则继续等待。返回 0 或严重错误时,logcat 输出 Unexpected EOF 或 read failure。

Unexpected EOF 的诊断信息明确提示三种可能:设备关机、logd 崩溃,或 logcat 消费速度跟不上日志产生速度。它不是“没有新日志”的普通结果;普通无新日志在 blocking 模式下应继续等待。

源码文件:system/logging/logcat/logcat.cpp

相关函数/类型:Logcat::Run 读取循环

cpp
while (!max_count_ || print_count_ < max_count_) {
    struct log_msg log_msg;
    int ret = android_logger_list_read(logger_list.get(), &log_msg);
    if (!ret) {
        error(EXIT_FAILURE, 0, "Unexpected EOF!");
    }
    if (ret < 0) {
        if (ret == -EAGAIN || ret == -EWOULDBLOCK || ret == -ETIMEDOUT) {
            // 非阻塞读取暂时没有更多记录,调用方可稍后再次读取。
            break;
        }
        if (ret == -EIO) error(EXIT_FAILURE, 0, "Unexpected EOF!");
        if (ret == -EINVAL) error(EXIT_FAILURE, 0, "Unexpected length.");
    }
    // ... 校验 log id、应用 UID 过滤并按 TEXT/BINARY/PROTO 输出
    ProcessBuffer(&log_msg);
}

这里把正常结束和失败分成了两类:非阻塞模式的 EAGAIN/timeout 是读取窗口结束,0 或 EIO 是异常 EOF, EINVAL 则说明数据长度非法。验证 -t 与 -T 时,输入应分别选择非阻塞和阻塞模式,断言前者在历史 记录读完后退出、后者继续等待;该实验只证明 logcat reader 的终止语义,不证明 logd 永远不会丢弃历史记录。

6.3 --wrap用途 ​

--wrap 使用 ANDROID_LOG_WRAP | ANDROID_LOG_NONBLOCK,让 reader 在 buffer 即将 wrap 时唤醒,适合轮询工具降低空转。Android 17 的代码支持可选 timeout 参数,但非默认 timeout 会打印 warning;这不是普通实时跟随的替代品。

7. 清空 ​

7.1 -c ​

bash
adb logcat -c
adb logcat -b system -c
adb logcat -f /data/local/tmp/log.txt -c

没有 -f 时,logcat 对选中的 log id 调用 android_logger_clear(),请求 logd 清空对应 ring buffer。带 -f 时,-c 改为删除输出文件及其 rotate 文件;这不是清空 logd。-L -c 则针对 pstore 文件,且与普通 logd 控制选项不兼容。

7.2 -g ​

-g 对每个选中 buffer 调用 get size,输出 ring buffer size、consumed、readable、最大 entry 和最大 payload。-G 16M 调用 set size;它改变 logd 为该 buffer 分配的容量,不会凭空恢复已经被覆盖的历史记录。

bash
adb logcat -b main -g
adb logcat -b main -G 16M

实际是否允许修改、最终容量是否受产品上限约束,由 logd 和设备权限决定。命令成功后应再次用 -g 读取结果,而不是仅凭命令返回码推断容量已经变成目标值。

7.3 -S ​

-S 请求 logd statistics,-p 查询 prune rules,-P 'LIST ...' 设置 prune rules。prune 规则以 UID、UID/PID 或 /PID 描述,~ 前缀改变优先级方向,特殊规则还可让 logd 自动处理最嘈杂 UID/PID。

这类控制命令不读取普通 log record,也不适合用来判断“日志是否产生”。它们改变的是 logd 的保留策略和观测统计。

8. 文件输出与轮转 ​

8.1 -f文件owner ​

bash
adb logcat -f /data/local/tmp/trace.log -r 1024 -n 5

Logcat::OpenLogFile() 以 append/create 方式打开目标文件,记录当前文件字节数;每次输出增加计数,达到 -r 的 KiB 阈值后执行 RotateLogs()。轮转通过 rename 把旧文件改名为 .1、.2 等,再重新打开主文件。

-r 必须和 -f 同时使用;-n 只限制保留的 rotated files,默认 4。它们控制 logcat 输出文件,不影响 logd ring buffer 的大小或 prune。

8.2 --id ​

--id=<id> 也要求 -f。logcat 将签名写入 <file>.id;当签名变化时,相关文件会被清理。这是为了避免不同 build/设备把旧格式或旧上下文的持久化日志拼在同一个文件里。

Android 的 logcatd.rc 以类似方式启动持久化 logcatd:选择 buffer、format、uid 等选项,并使用 -f、-r、-n 写入 /data/misc/logd/logcat。这属于 init/property 驱动的设备服务,不等于每次手动 adb logcat -f 都会写入同一目录。

8.3 文件失败 ​

文件目录不可写、路径不存在、rename 失败时,logcat 进程可能退出,但 logd 仍继续接收和保存日志。反过来,logd 读失败时,即使 stdout 可写,logcat 也不能继续输出真实数据。故障报告应分别记录 source reader 和 output writer 的错误。

9. 权限和产品差异 ​

日志读取不是无条件的全局观察能力。普通应用、shell、root、system 和 Device Owner 看到的 buffer 与 UID 范围可能不同:

  • kernel 只在支持的 userdebug/eng 等产品变体提供;
  • security 需要 Device Owner 或相应系统能力;
  • --uid 读取其他用户日志需要 root/log/system 等权限;
  • pstore 依赖内核和设备启动链是否提供 /sys/fs/pstore;
  • logd buffer size、prune 和 persistent logging 可能由产品 property 限制。

因此 adb logcat -b all 在一台设备上成功,不代表另一台产品也能打开全部 buffer。命令输出中的 Unable to open log device 要和“某个 tag 没有记录”严格区分:前者是 buffer/权限/设备能力,后者是过滤或日志产生时机。

权限失败不是空日志

如果指定 buffer 打不开,先处理 buffer 可见性、设备构建类型和权限;不要把权限拒绝当作“应用没有打印日志”,也不要用扩大 buffer 或改变格式来解决权限问题。

10. Logcat测试 ​

10.1 基础测试 ​

测试主题输入/动作断言方向能证明什么
buckets向多个 buffer 写入唯一 tag输出包含对应记录buffer 选择和读取组合
event tag filter写入 event 并使用 tag/filter只出现预期事件binary event 与 filter 交互
format/time-v long/year/nsec/epoch字段和时间格式匹配formatter 语义
tail-t 3/10/100输出行数和历史顺序tail count
blocking启动 reader 后再写日志reader 被唤醒blocking socket 读取
get_size-gsize/readable/consumed 输出控制请求与格式
logrotate-f -r -n 写入大量日志轮转文件数量/大小output writer 轮转

End_to_End 类测试把写入、读取和命令组合起来;LogBufferTest、SerializedLogBufferTest 则更接近 logd 存储顺序和容量边界,不会替 logcat formatter 做断言。

10.2 测试边界 ​

host 单元或 logcat device test 可以证明解析、读取和轮转分支,但不能证明每个产品都开放 security、kernel、pstore,不能证明厂商 logd property 没有覆盖默认容量,也不能证明应用在当前用户真的有写日志权限。测试结论必须保留这些边界。

11. 读取流程 ​

11.1 reader ​

bash
adb logcat -g
adb logcat -b main -d

如果 -g 都无法打开指定 buffer,先处理权限或设备能力;如果 -g 成功而 -d 没有记录,再看时间窗口、过滤器和日志产生时机。

11.2 流程入口 ​

bash
adb logcat -c
adb logcat -b main -v threadtime '*:V'

清空会破坏历史证据,只适合明确开始新的实验;不要在生产故障收集前随手执行。用最宽的合法过滤观察一次,再逐步增加 *:S、tag priority、--pid 和 regex。

11.3 区分历史 ​

bash
adb logcat -b main -t 200 -v threadtime
adb logcat -b main -T 200 -v threadtime
adb logcat -b main -f /data/local/tmp/main.log -r 1024 -n 3

第一条应结束,第二条应继续等待,第三条将由设备 logcat 写文件并轮转。若第三条失败,分别检查文件目录权限和 logd reader,而不要只换成 -d。

11.4 复现上下文 ​

日志诊断记录至少应包含:

类别内容
设备serial、build variant、Android release、当前 user
readerbuffer、-t/-T/-d、pid/uid、是否 blocking
filter完整 FILTERSPEC、ANDROID_LOG_TAGS、regex
format-v 主格式和 adverb、binary/proto
control是否执行 -c、-G、-P、-S
outputstdout 或文件、rotate size/count、文件路径
failure原始 stderr、首次失败阶段、是否出现 EOF

缺少这些字段,后续读者无法判断“没有日志”究竟是没有产生、被 filter 丢弃、buffer 不可见、历史窗口不覆盖,还是输出文件失败。

12. 时间线顺序 ​

12.1 读取顺序 ​

选择多个 buffer 后,liblog 返回的是多个日志源的交错记录。日志条目带有 realtime 时间戳、PID、TID、UID 和 buffer id,但时间相近不等于调用关系。不同线程可以并行写入,进程调度和 logd socket 处理也会影响读者看到的相邻顺序。

下面两条记录即使只差几十微秒,也不能仅凭显示顺序证明第二条由第一条直接触发:

text
08-18 10:20:30.123456  1000  1234 I ActivityManager: start process
08-18 10:20:30.123482  2000  2210 D MyApp: Application.onCreate

可靠的因果证据应结合 transaction id、request id、PID/TID、组件名、状态变化或源码调用链。-v usec/nsec 只能提高显示精度,不能把并发系统变成串行系统。

12.2 realtime ​

threadtime 默认显示 wall-clock 日期和时间,适合人类对照事件;epoch 便于脚本排序和与其他 Unix 时间源比较;monotonic 接近开机后的 CPU/系统时间轴,适合分析 wall clock 被校时或时区变化的场景。

bash
adb logcat -v threadtime,usec
adb logcat -v epoch,usec
adb logcat -v monotonic,usec

不要把不同时间基准直接相减。NTP/网络校时、用户改时间和时区变化可能让 realtime 跳变,而 monotonic 不用于表示自然日期。跨设备比较时还要确认两台设备的时间源是否同步。

12.3 divider ​

-D 调用 PrintDividers(),在输出切换 buffer 时打印 divider。它不会改变合并排序,但能避免把 events/security 的格式化记录误认为 main 日志。写入文件用于后续分析时,保留 divider 或输出 proto/binary 中的 buffer id,比只截取消息正文可靠。

13. 生命周期 ​

13.1 --pid ​

bash
adb logcat --pid=1234

--pid 在 android_logger_list_alloc() 时传入 reader。它不会根据 package name 自动追踪重启后的新 PID。应用崩溃后由 system server 拉起,新进程通常获得不同 PID,旧命令就可能继续等待却没有新记录。

要诊断“启动后崩溃并重启”的应用,应该同时保留:

  • ActivityManager/system_server 中的进程创建与死亡记录;
  • crash buffer 中的异常信息;
  • main buffer 中新旧 PID 的应用日志;
  • package/process name 到 PID 的时间映射。

只运行一次 pidof 再永久跟踪 --pid,会丢失进程重启后的日志。

13.2 --uid ​

同一应用在一次安装和一个用户下拥有稳定 UID,但多用户会把 user id 编入 UID,isolated process 还可能使用独立 UID。--uid=UIDS 支持数值列表,不执行用户名解析;源码在收到每条 log_msg 后比较 entry UID,不匹配就跳过。

因此,--uid 比 --pid 更适合覆盖普通应用进程重启,却不能自动包含 isolated/service sandbox UID,也不能跨用户把同一 package 的不同 UID 归并为一个身份。

13.3 tag过滤边界 ​

Java/Kotlin 的 tag 可以由开发者选择,native 组件也可以复用 tag。MyApp:* 不能证明所有记录都来自某个 package,包内组件也不一定都使用同一 tag。面向教材的诊断应组合 tag、UID、PID 和时间窗口,而不是把一个 tag 当作应用身份主键。

14. 崩溃 ​

14.1 保留历史边界 ​

故障已经发生时,第一条命令不应是 logcat -c。更稳妥的顺序是:

bash
adb logcat -b main,system,crash -t 2000 -v threadtime,year,usec > before.txt
adb logcat -b main,system,crash -T 200 -v threadtime,year,usec > follow.txt

第一条冻结当前历史窗口并退出,第二条从最近记录开始持续跟随。> 在 host shell 中重定向,因此文件写在开发机;这与设备端 adb logcat -f /data/... 不同。

如果使用 host 重定向,输出通过 ADB shell 返回到 host,host 磁盘和终端管道是 output owner;如果使用 -f,设备 logcat 直接打开设备路径。两者的权限、空间和断线行为不同。

14.2 crash buffer ​

crash buffer 适合定位 fatal exception、native crash 等关键记录,但进程启动、组件调度、权限检查和服务状态常在 main/system。只读取 -b crash 会失去崩溃前的业务路径;只读取 main 又可能错过专门的 crash 记录。

ANR 的最终报告还可能涉及 traces、dropbox 或 bugreport 等其他证据。logcat 能显示 system server 的 ANR 判定和相关消息,但不能替代完整 ANR trace。本文只把它作为时间线入口,不把 logcat 输出冒充全部诊断材料。

14.3 buffer wrap ​

logd ring buffer 满后会按保留/prune 策略覆盖旧记录。增大 -G 只能影响后续容量,已经覆盖的数据无法恢复。高频日志设备出现 Unexpected EOF 或历史窗口不足时,应该减少无关日志、提高消费速度、合理扩大 buffer,或启用受控持久化,而不是反复执行更大的 -t。

清空和扩容都会改变现场

logcat -c 会删除当前 buffer 证据,logcat -G 会改变设备日志配置。故障现场应先导出历史与统计,再决定是否改变 buffer 状态,并在记录中标明操作时间。

15. pstore 日志 ​

15.1 -L读取边界 ​

bash
adb logcat -L -b all -d

-L 设置 ANDROID_LOG_PSTORE | ANDROID_LOG_NONBLOCK,数据来自 pstore,而不是当前运行的 logd ring buffer。它用于观察上次重启前保存的日志,前提是内核、设备和日志写入链路确实配置了 pstore。

源码明确禁止 -L 与 -g/-G、-S、-p/-P 组合,因为当前 logd 的容量、统计和 prune 不适用于 pstore。-L -c 会尝试删除特定 pstore 文件;这同样是破坏性操作。

15.2 pstore ​

空结果至少有几种可能:设备未提供对应 pstore 节点、上次启动没有写入、启动后已清理、权限不允许,或选择的 buffer 没有记录。必须先确认 pstore 文件和产品能力,不能简单得出“上次没有崩溃”。

16. 输出协议消费者 ​

16.1 binary输出 ​

普通 threadtime 把字段渲染成一行文本,适合 grep 和人工阅读。消息中换行、不可打印字符和 binary event 需要 printable 或 descriptive 处理;重新用正则解析文本时,必须固定 format,否则字段位置会变化。

16.2 proto输出 ​

-B 直接输出 log_msg,包含 header 和 payload。它适合与 Android logging 工具链配合,不适合作为长期稳定的自定义文件协议。读取端应和对应 release 的结构定义匹配,并验证 record length 和 log id。

16.3 消费输出 ​

--proto 使用 logcat.proto 定义,logcat 把每条 AndroidLogEntry 转换为 LogcatEntryProto,并在前面写 8 字节长度。消费者应循环读取 length,再读取完整 protobuf;直接把整个文件当作单个 protobuf message 会失败。

选择输出类型时可以按消费者决定:人读用固定文本格式,现有 Android 原始工具用 binary,跨语言结构化流水线优先 proto。不要为了“信息更多”盲目选择 binary,最终却没有可靠解析器。

17. 诊断实验 ​

17.1 验证 -t ​

先写入一个唯一 tag,然后分别运行:

bash
adb logcat -t 5 UniqueTag:V '*:S'
adb logcat -T 5 UniqueTag:V '*:S'

第一条读完历史后应退出;第二条显示历史后继续阻塞。再写一条新日志,第二条应收到它。这个实验验证 non-blocking flag,而不是验证 buffer 容量。

17.2 FILTERSPEC ​

让同一 tag 写入包含 start、timeout 和 done 的三条消息:

bash
adb logcat -d '*:S' UniqueTag:V --regex='timeout'

FILTERSPEC 先保留 UniqueTag,再由 regex 从消息中选择 timeout。若 tag filter 已经把记录丢弃,放宽 regex 也不会恢复它。

17.3 验证文件轮转 ​

在可写目录运行小阈值轮转,并持续产生唯一日志:

bash
adb logcat -f /data/local/tmp/rotate.log -r 64 -n 3 UniqueTag:V '*:S'

检查主文件和 .1~.3 的数量、大小与修改时间。测试证明 output writer 的 rename/reopen 行为,不证明 logd buffer 没有 wrap,也不证明 host 已经保存这些设备文件。

17.4 诊断命令 ​

把 -f、-g、-S 混在一起运行,或把 -L 与 -G 混用,logcat 会在打开 reader 前拒绝参数组合。这个行为很有价值:它说明控制 logd 的命令、读取 pstore 的命令和输出到文件的命令不是可以任意叠加的 flags。排查时先拆成独立命令,分别保存输出,才能知道失败来自参数互斥还是设备服务。

18. 源码导航 ​

推荐按以下顺序阅读 Android 17 代码:

  1. packages/modules/adb/client/commandline.cpp::logcat:确认 host 只做命令拼接和 shell 路由;
  2. system/logging/logcat/logcat.cpp::show_help:获得 Android 17 实际选项和产品说明;
  3. Logcat::Run:追踪 getopt、buffer mask、filter、tail、控制操作和读取循环;
  4. Logcat::ProcessBuffer:区分普通文本与 binary event/stat/security;
  5. liblog/logger_read.cpp:理解 logger list、每个 log id 的 fd 和阻塞/非阻塞读取;
  6. liblog/include/log/log_read.h:确认 clear、size、statistics、prune 的接口边界;
  7. logd/LogBuffer*:理解 ring buffer 排序、容量和 serialized buffer 测试;
  8. logcat/logcat_test.cpp:用测试反向确认格式、tail、blocking、rotate 和端到端行为。

19. 日志状态 ​

adb logcat 相关状态可以归纳为四个层次:

  • 产生状态:应用、framework、kernel 是否写入目标 buffer;
  • 保存状态:logd ring buffer 容量、prune 和是否已经 wrap;
  • 读取状态:logger list 的 buffer、pid/uid、tail、blocking 和权限;
  • 呈现状态:tag priority、regex、format、binary/proto 与文件轮转。

前一层没有数据,后一层无法创造数据;后一层过滤掉数据,也不能据此证明前一层没有产生数据。掌握这四层,才能把 adb logcat 从“查看输出的命令”提升为一条可解释、可复现的 Android 17 日志读取链路。