关于 extracting-logs-with-logcat
这是一份面向 Android 开发者的 `adb logcat` 操作指南,涵盖实时读取、筛选、格式化和持久化设备日志,并介绍 CI 故障捕获及发布版日志剔除。
主要功能
- 01实时读取或转储日志
- 02选择日志缓冲区并筛选
- 03设置格式、时间和 PID 过滤
- 04将日志写入文件并轮转
- 05使用 R8 规则剔除调试日志
适用场景
- 01在 CI 测试失败时保存设备日志
- 02过滤应用或系统组件日志
- 03采集结构化 JSON 日志
- 04排查进程重启后的日志记录
- 05移除发布版本中的 `Log.d` 调用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill extracting-logs-with-logcat安装后在 Claude Code 中输入/extracting-logs-with-logcat确认已加载。
详细介绍
使用 logcat 提取日志 — 读取设备日志
本技能涵盖 adb logcat 的端到端用法:流式输出与转储、缓冲区选择、过滤表达式、格式标志、PID 和时间过滤、文件轮转,以及在发布版本中剔除 Log.d 的 R8 规则。配套的 CI 失败时捕获模式见 ../../automation/scripting-adb-for-ci/SKILL.md。
何时使用此技能
- CI 上的测试失败;开发者需要将设备日志转储到构件归档中。
- 开发者的
adb logcat | grep MyApp淹没了 SSH 会话——在 logcat 端进行过滤即可解决。 - 某个场景只会在
system_server或crash缓冲区中复现,而不会出现在默认的main缓冲区中。 - 开发者想要结构化日志(
-v json),以便机器读取。 Log.d("Sensitive", "...")调用似乎泄漏到了发布版本中——缺少 R8-assumenosideeffects规则。- 应用被 Doze/ANR 终止后日志消失——需要使用
--pid并循环重新解析 PID。
何时不使用此技能
- 捕获屏幕截图或视频。请使用
../../capture/capturing-screenshots-and-screenrecord/SKILL.md。 - 从设备拉取通用文件。请使用
../../transfer/extracting-test-artifacts/SKILL.md。 - 整套脚本,包括重试、端口转发和多设备并行分发。请使用
../../automation/scripting-adb-for-ci/SKILL.md。 - 执行手势或更改设置。请使用
../../control/injecting-input-and-state/SKILL.md。
前提条件
adb get-state返回device。请参阅../../devices/connecting-to-devices/SKILL.md。- 使用
-v json:设备 API 30+(Android 11+)。搭配-v UTC -v year可获得明确无歧义的时间戳。 - 按 PID 过滤:
adb shell pidof -s <pkg>需要 API 24+(toyboxpidof -s);在较旧版本上使用adb shell ps -A | grep <pkg>。
三种基本操作
| 命令 | 行为 |
|---|---|
adb logcat | 将设备日志流式输出到 stdout;持续运行,直到被中断。 |
adb logcat -d | “转储日志并退出。”快照模式,非常适合 CI。 |
adb logcat -c | “清除日志缓冲区。”复现错误前先运行此命令。 |
(引自 developer.android.com/tools/logcat。)
惯用的 CI 捕获模式:
adb logcat -c # clear before scenario
./run-scenario.sh # reproduce
adb logcat -d > artifacts/log.txt # dump after
缓冲区选择
-b <name> 用于选择要读取的内核/用户空间环形缓冲区。缓冲区说明见 developer.android.com/tools/logcat:
| 缓冲区 | 内容 |
|---|---|
main | 默认的应用端缓冲区。不包含系统/崩溃日志。 |
system | 框架 / system_server 消息。 |
crash | Tombstone 和未处理异常输出。 |
events | 结构化/二进制系统事件缓冲区。搭配 -v descriptive 可解码标签名称。 |
radio | 无线电/电话相关消息。 |
kernel | 内核缓冲区。 |
all | 所有缓冲区。 |
default | 隐式集合:main、system、crash。 |
多个 -b 标志和逗号分隔列表都可使用:
adb logcat -b radio
adb logcat -b main -b radio -b events
adb logcat -b main,radio,events
events 缓冲区适合查找 am_*、wm_*、input_focus、notification_*——这些由框架为检测用途发出,而非供人阅读。使用以下命令解码标签名称:
adb logcat -b events -v descriptive
优先级阶梯与过滤表达式
过滤规范由以空格分隔的 tag:priority 对组成。* 匹配所有标签。
| 字母 | 含义 |
|---|---|
V | 详细 |
D | 调试 |
I | 信息 |
W | 警告 |
E | 错误 |
F | 致命 |
S | “静默(最高优先级,不输出任何内容)” |
设置 *:S 会静默所有内容,随后之前指定的任何 tag:P 都会以优先级 P 或更高级别重新启用该标签。仅显示应用日志的规范写法:
adb logcat ActivityManager:I MyApp:D *:S
“抑制所有日志,但保留 ActivityManager(Info 及以上)和 MyApp(Debug 及以上)”——developer.android.com/tools/logcat。
其他常用写法:
adb logcat *:W # warnings and above, all tags
adb logcat *:E # errors and above (very common in CI)
如果当前工作目录中有名为 S 的文件,zsh/bash 会在未加引号时对 *:S 执行通配符展开——编写脚本时请加引号:
adb logcat "ActivityManager:I MyApp:D *:S"
也可以通过环境变量将同一过滤器设为主机默认值:
export ANDROID_LOG_TAGS="ActivityManager:I MyApp:D *:S"
格式标志 — -v <format>
| 格式 | 输出内容 |
|---|---|
brief | "显示优先级、标签和 PID" |
long | "所有元数据字段,消息之间以空行分隔" |
process | "仅 PID" |
raw | "不含元数据的原始日志消息" |
tag | "仅优先级和标签" |
thread | "旧版格式,显示优先级、PID 和 TID" |
threadtime | 默认。"日期、时间、优先级、标签、PID 和 TID" |
time | "日期、时间、优先级、标签和 PID" |
格式修饰符可与 -v 叠加(可用逗号组合或重复指定):
| 修饰符 | 效果 |
|---|---|
color | 按优先级着色。 |
descriptive | 解码事件日志标签名称。 |
epoch | 显示自 1970-01-01 起的秒数。 |
monotonic | 显示自上次启动以来的 CPU 秒数。 |
printable | 转义二进制内容。 |
uid | 显示日志进程的 UID 或 Android ID。 |
usec | 以微秒精度显示时间。 |
UTC | 以 UTC 显示时间。 |
year | 在显示的时间中添加年份。 |
zone | 添加本地时区。 |
adb logcat -v json -v UTC -v year # Android 11+; structured stream, unambiguous time
adb logcat -b all -v color -d # color dump, all buffers
默认行示例(developer.android.com/tools/logcat):
I/ActivityManager( 585): Starting activity: Intent { action=android.intent.action.MAIN ... }
brief 的格式为:<priority>/<tag>(<PID>): <message>。threadtime 会添加日期、时间和 TID。
时间和 PID 筛选器
-T '<date> <time>'— 从给定时间戳及之后的第一条记录开始。格式与threadtime相同,例如'01-01 12:00:00.000'。-T <count>— 输出缓冲区中的最后 N 条记录,然后继续流式输出。-t <count>— 输出最后 N 条记录,然后退出(也就是说,-t相当于会自动退出的-T)。--pid <pid>— 仅输出单个进程的记录。可与pidof结合使用:
adb logcat --pid=$(adb shell pidof -s com.example.app)
pidof -s 会返回单个 pid;不带 -s 时会返回以空格分隔的列表,--pid 无法直接接受该列表。如果应用可能被终止并重启(Doze、ANR、崩溃),请使用循环重新解析 PID:
while true; do
PID=$(adb shell pidof -s com.example.app 2>/dev/null)
[ -n "$PID" ] && adb logcat --pid="$PID"
sleep 1
done
缓冲区轮转与持久化
对于长时间捕获,可通过轮转将内容持久化到文件:
| 标志 | 含义 |
|---|---|
-f <file> | 写入 <file>,而不是标准输出。**注意:**使用 adb logcat -f 时,文件位于主机端;使用 adb shell logcat -f 时,路径位于设备端。 |
-r <kbytes> | 每 N KB 轮转一次输出。需要 -f。 |
-n <count> | 最多保留 <count> 个轮转文件(logfile、logfile.1,……)。 |
-G <size> | 调整所选 -b 环形缓冲区的内核日志缓冲区大小(例如 1M、16M)。在 logd 的整个运行期间保持有效。 |
-g | 打印缓冲区大小和当前使用量,然后退出。用于诊断“为什么缺少日志?”的低开销方法。 |
adb logcat -G 16M # grow the main buffer
adb logcat -g # inspect sizes
adb logcat -b main -f /data/local/tmp/app.log \
-r 1024 -n 8 # 8x ~1MB rotated files (device-side)
adb shell logcat -b main -f /data/local/tmp/app.log \
-r 1024 -n 8 # same, but the device runs it
-G 调整的是该缓冲区的全局大小,并会在 logd 重启后继续生效(直到重启设备)。不要在内存受限的设备上设置为 16 MB 后就忘记;如果 CI 集群由多人共用,请在测试套件运行后用 -G 256K 释放内存。
在发布版本中移除 Log.d — R8 规则
除非通过代码缩减将其移除,否则 android.util.Log 调用会保留在发布版 APK 中。proguard-rules.pro 中的标准写法(R8/ProGuard):
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static boolean isLoggable(java.lang.String, int);
}
-assumenosideeffects 允许 R8 将列出的方法视为纯空操作,因此所有调用(以及参数为常量时的字符串构造器表达式)都会被移除。生产环境诊断应保留 w、e、wtf。使用 Timber 时:移除由 BuildConfig.DEBUG 控制的 DebugTree 初始化,并依赖将警告和错误转发到 Crashlytics 的 ReleaseTree。
对于 Compose,不要对 kotlin.jvm.internal.Intrinsics 使用 -assumenosideeffects ——这会移除编译器依赖的空值检查。
模式
模式:错误做法 — 使用 adb logcat | grep MyApp 进行筛选
# WRONG
adb logcat | grep MyApp
# WRONG because: the device sends every log line over the USB/TCP transport, then the host
# discards 99% via grep. On a busy device this saturates the transport and the grep can lag
# behind real time. Worse, `grep` runs without -F, so a tag that contains regex metacharacters
# (`.`, `(`, `+`) silently mismatches.
# RIGHT — filter at the logcat level
adb logcat MyApp:D *:S
设备只会输出匹配的行;传输不会拥塞。
模式:错误做法 — 认为 -T <count> 会退出
# WRONG
adb logcat -T 100 > tail.txt
# Hangs forever — -T <count> prints the last 100 lines THEN CONTINUES STREAMING.
# RIGHT — `-t` is `-T` with auto-exit
adb logcat -t 100 > tail.txt
模式:用于机器摄取的结构化日志
adb logcat -v json -v UTC -v year -d -b all > run.jsonl
每行都是一个 JSON 对象——通过管道传给 jq:
jq -c 'select(.priority == "ERROR")' run.jsonl
(设备需运行 Android 11 或更高版本才能使用 -v json。)
模式:失败时捕获 — 清除、运行、转储
adb logcat -c
./run-scenario.sh
RC=$?
if [ "$RC" -ne 0 ]; then
adb logcat -d -v threadtime > artifacts/logcat.txt
adb logcat -d -b crash -v threadtime > artifacts/crash.txt
adb logcat -d -b events -v descriptive > artifacts/events.txt
fi
exit "$RC"
-c 会在运行前清除日志,因此转储只会捕获该场景的日志。
模式:绑定 PID 的流,可在进程重启后继续工作
while true; do
PID=$(adb shell pidof -s com.example.app 2>/dev/null)
if [ -n "$PID" ]; then
adb logcat --pid="$PID" -v threadtime
fi
sleep 1
done
外层循环会在 Doze、ANR 或崩溃导致应用被杀并重启后,重新解析 PID。
模式:-f 的主机端与设备端
# host-side rotation (file lives on the developer machine)
adb logcat -b main -f ~/logs/app.log -r 1024 -n 8
# device-side rotation (file lives in /data/local/tmp on the device)
adb shell logcat -b main -f /data/local/tmp/app.log -r 1024 -n 8
adb pull /data/local/tmp/app.log ~/logs/app.log # later
强制规则
- 必须在 logcat 层级进行过滤(例如
MyApp:D *:S),而不是通过主机端的grep过滤。在设备上过滤可避免传输通道饱和。 - 必须在 zsh/bash 中为包含
*的过滤字符串加引号(例如"MyApp:D *:S")——如果当前工作目录中存在名为S的文件,通配符会被展开。 - 必须使用
-t <count>实现“读取末尾日志后退出”。-T <count>会在初始转储后继续流式输出。 - 必须在 CI 中使用
-d进行快照转储。流式运行adb logcat需要显式终止。 - 必须在复现需要附加日志转储的 Bug 前调用
adb logcat -c。否则,转储会包含无关的历史记录。 - 必须区分
adb logcat -f <file>(主机端路径)与adb shell logcat -f <file>(设备端路径)。两种 shell 形式的行为并不相同。 - 必须在发布曾调用
Log.d记录敏感数据的 APK 时,为android.util.Log的v/d/i(以及isLoggable)添加 R8-assumenosideeffects规则。保留w/e/wtf。 - 不得对
kotlin.jvm.internal.Intrinsics使用-assumenosideeffects——这会移除 Kotlin 编译器所依赖的空值检查语义。 - 不得在 Android 11 以下的设备上依赖
-v json——它会静默输出空流。 - **优先:**使用
-v threadtime,year,UTC(或-v threadtime -v year -v UTC),以便在 CI 产物中获得明确无歧义的绝对时间戳。 - **优先:**捕获缓慢发生的 Bug 时,使用
-G 16M调整main缓冲区大小,并在清理阶段恢复默认值。
验证
-
scripts/或.github/workflows/中没有脚本将adb logcat的输出直接传递给grep进行过滤。所有过滤器都作为 logcat 参数传入。 - CI 失败时捕获的转储至少包含
main、crash和events缓冲区。 - 发布 APK 不会输出
Log.d调用(通过apkanalyzer dex packages app-release.apk | grep 'android.util.Log'验证,结果仅包含e/w/wtf)。 - shell 脚本中包含
*:S的过滤字符串均已加引号。 - 所有
--pid调用都使用pidof -s <pkg>(单数),并在应用可能重启时通过循环重新解析 PID。 - CI 中每个长时间运行的
adb logcat会话都设有墙钟时间上限(timeout 600s adb logcat ...),以免卡住的会话占满磁盘。
参考资料
- developer.android.com/tools/logcat — 缓冲区表、优先级梯级、过滤表达式、
-v threadtime/json/descriptive、格式修饰符、-T/-t/--pid/-r/-n/-G/-g标志、行格式架构。 - developer.android.com/tools/adb —
adb logcat主机端简写与adb shell logcat的区别。 - developer.android.com/studio/build/shrink-code — R8
-assumenosideeffects和android.util.Log剥离规则。 - developer.android.com/reference/android/util/Log —
Log.v/d/i/w/e/wtf/isLoggable签名。 - 研究笔记
tasks/research/A3-adb-observability-automation.md— 完整的 logcat 功能、-f主机端与设备端的细微区别、R8 保留规则的理由、CI 失败时捕获日志的模式。 - 同级技能:
../../architecture/understanding-adb-architecture/SKILL.md— 服务端 / 守护进程、传输。 - 同级技能:
../../devices/connecting-to-devices/SKILL.md— 设备状态、wait-for-device。 - 同级技能:
../../devices/connecting-over-wifi/SKILL.md—adb pair/adb connect。 - 同级技能:
../../apps/installing-and-managing-apps/SKILL.md— 安装要捕获其日志的 APK。 - 同级技能:
../../tests/running-instrumented-tests-via-adb/SKILL.md—am instrument -w -r与 logcat 的交互。 - 同级技能:
../../control/injecting-input-and-state/SKILL.md— 执行手势,并在 logcat 中查看其影响。 - 同级技能:
../../capture/capturing-screenshots-and-screenrecord/SKILL.md— 配套的屏幕截图捕获。 - 同级技能:
../../transfer/extracting-test-artifacts/SKILL.md— 从设备拉取轮转后的日志文件。 - 同级技能:
../../automation/scripting-adb-for-ci/SKILL.md— 完整的 CI 失败时捕获日志 / 重试 / 清理模式。 - 跨集合:
../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md— 与 logcat 交错显示的检测日志。 - 跨集合:
../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md— 在events缓冲区中显示的 Activity 生命周期事件。 - 跨集合:
../../../fundamentals/strategies/applying-testing-strategies/SKILL.md— 何时适合将日志捕获作为可观测性层。