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 ADB | client/commandline.cpp::logcat | argv、ANDROID_LOG_TAGS 拼接 | 设备 shell service |
| logcat | logcat.cpp::Logcat::Run | buffer mask、format、tail、regex、输出文件 | liblog reader 和 stdout/file |
| liblog | logger_read.cpp | logger list、每个 log id 的 fd、读取模式 | logd socket |
| logd | LogBuffer 等 | 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() 保护其内容,再构造:
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 主日志 | 默认集合之一 |
system | system/server 相关日志 | 默认集合之一 |
crash | 崩溃相关日志 | 默认集合之一 |
kernel | 内核日志桥接 | userdebug/eng 等产品条件 |
radio | telephony/radio | 需显式选择,产品可能不可用 |
events | event tag 二进制日志 | 读取时需要 tag map 解码 |
security | 安全日志 | Device Owner 或相应权限条件 |
stats | 统计相关日志 | 设备权限和实现条件相关 |
没有任何 -b 时,Android 17 源码将 mask 设为 main,system,crash,kernel。-b default 选择前三个,不包含 kernel;-b all 把 mask 设为全部 bit。多个 -b 或逗号分隔列表会交给同一个解析分支。
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
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 过滤输出
过滤规则形如:
<tag>[:priority]优先级从 V、D、I、W、E、F 到 S。例如:
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 同时使用时才有特殊意义,会继续打印非匹配消息直到达到匹配计数。
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。其他常用主格式包括:
| 格式 | 主要字段 |
|---|---|
brief | priority、tag、PID |
long | 完整 metadata,并用空行分隔消息 |
process | PID |
raw | 原始消息文本 |
tag | priority 与 tag |
thread | priority、PID、TID |
time | 日期、时间、priority、tag、PID |
threadtime | 日期、时间、priority、tag、PID、TID |
adverb 可以组合:color、descriptive、epoch、monotonic、printable、uid、usec、UTC、year。例如:
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 logcat | blocking | 读取历史后继续等待新记录 |
adb logcat -d | non-blocking | dump 当前可读记录后退出 |
adb logcat -t 100 | non-blocking | 取最近 100 行并退出 |
adb logcat -T 100 | blocking | 从最近 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 读取循环
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
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 分配的容量,不会凭空恢复已经被覆盖的历史记录。
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
adb logcat -f /data/local/tmp/trace.log -r 1024 -n 5Logcat::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 | -g | size/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
adb logcat -g
adb logcat -b main -d如果 -g 都无法打开指定 buffer,先处理权限或设备能力;如果 -g 成功而 -d 没有记录,再看时间窗口、过滤器和日志产生时机。
11.2 流程入口
adb logcat -c
adb logcat -b main -v threadtime '*:V'清空会破坏历史证据,只适合明确开始新的实验;不要在生产故障收集前随手执行。用最宽的合法过滤观察一次,再逐步增加 *:S、tag priority、--pid 和 regex。
11.3 区分历史
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 |
| reader | buffer、-t/-T/-d、pid/uid、是否 blocking |
| filter | 完整 FILTERSPEC、ANDROID_LOG_TAGS、regex |
| format | -v 主格式和 adverb、binary/proto |
| control | 是否执行 -c、-G、-P、-S |
| output | stdout 或文件、rotate size/count、文件路径 |
| failure | 原始 stderr、首次失败阶段、是否出现 EOF |
缺少这些字段,后续读者无法判断“没有日志”究竟是没有产生、被 filter 丢弃、buffer 不可见、历史窗口不覆盖,还是输出文件失败。
12. 时间线顺序
12.1 读取顺序
选择多个 buffer 后,liblog 返回的是多个日志源的交错记录。日志条目带有 realtime 时间戳、PID、TID、UID 和 buffer id,但时间相近不等于调用关系。不同线程可以并行写入,进程调度和 logd socket 处理也会影响读者看到的相邻顺序。
下面两条记录即使只差几十微秒,也不能仅凭显示顺序证明第二条由第一条直接触发:
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 被校时或时区变化的场景。
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
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。更稳妥的顺序是:
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读取边界
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,然后分别运行:
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 的三条消息:
adb logcat -d '*:S' UniqueTag:V --regex='timeout'FILTERSPEC 先保留 UniqueTag,再由 regex 从消息中选择 timeout。若 tag filter 已经把记录丢弃,放宽 regex 也不会恢复它。
17.3 验证文件轮转
在可写目录运行小阈值轮转,并持续产生唯一日志:
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 代码:
packages/modules/adb/client/commandline.cpp::logcat:确认 host 只做命令拼接和 shell 路由;system/logging/logcat/logcat.cpp::show_help:获得 Android 17 实际选项和产品说明;Logcat::Run:追踪 getopt、buffer mask、filter、tail、控制操作和读取循环;Logcat::ProcessBuffer:区分普通文本与 binary event/stat/security;liblog/logger_read.cpp:理解 logger list、每个 log id 的 fd 和阻塞/非阻塞读取;liblog/include/log/log_read.h:确认 clear、size、statistics、prune 的接口边界;logd/LogBuffer*:理解 ring buffer 排序、容量和 serialized buffer 测试;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 日志读取链路。
