Android 7系统日志(五)日志读取—logcat源码深度分析

系列目录第一篇:全景图与架构概览 | 第二篇:logd守护进程—启动、初始化与Socket通信 | 第三篇:liblog库—日志写入的完整链路 | 第四篇:日志写入接口—Java层与Native层 | 第五篇:日志读取—logcat源码深度分析 | 第六篇:日志缓冲区管理—容量、裁剪与统计机制 | 第七篇:实战调试与常见问题分析


从源码角度解释 logcat 各种参数的工作原理。Android 7 中 logcat 是单个 C++ 文件,配合 logprint 库完成格式化与过滤。

一、logcat 源码概览

system/core/logcat/
├── logcat.cpp          ← 主源文件(约 1300 行)
├── event.logtags       ← events tag 映射表
├── logcatd.rc          ← logcatd 服务 rc 文件
├── logpersist          ← 日志持久化脚本
└── tests/              ← 测试

涉及的关键库文件:

system/core/liblog/logprint.c       ← 格式化和过滤逻辑
system/core/liblog/logger_read.c    ← 日志读取 API
system/core/include/log/logprint.h  ← 格式化/过滤 API 头文件
system/core/include/log/log_read.h  ← 日志读取 API 头文件

关键点:logcat 本身不直接操作 socket,而是通过 liblog 提供的 API(android_logger_list_allocandroid_logger_openandroid_logger_list_read)来读取日志。格式化和过滤逻辑也不在 logcat.cpp 中,而是在 liblog/logprint.c 中。


二、main() 入口 — 三层结构

源码路径system/core/logcat/logcat.cpp

int main(int argc, char **argv)
{
    // ===== 阶段1:参数解析(getopt_long) =====
    // 解析 -b/-v/-c/-g/-d/-t/-T/-e/-m/-s/-f/-n/-r/-L/-B/-S/-p/-P/-G/-D 等

    // ===== 阶段2:初始化日志读取器 =====
    // 2.1 确定缓冲区列表(默认 main + system + crash)
    // 2.2 设置输出格式(默认 threadtime)
    // 2.3 解析过滤器(命令行 / ANDROID_LOG_TAGS / 内核 cmdline)
    // 2.4 分配 logger_list,为每个缓冲区 open logger

    // ===== 阶段3:命令模式或读取循环 =====
    // 如果是 -c/-g/-G/-p/-P/-S:执行命令后退出
    // 否则进入主循环:android_logger_list_read() → 过滤 → 格式化 → 输出
}

三、-b 参数 — 缓冲区选择

3.1 默认缓冲区

源码路径system/core/logcat/logcat.cpp

// 未指定 -b 时的默认值
if (!devices) {
    dev = devices = new log_device_t("main", false);
    g_devCount = 1;
    if (android_name_to_log_id("system") == LOG_ID_SYSTEM) {
        dev = dev->next = new log_device_t("system", false);
        g_devCount++;
    }
    if (android_name_to_log_id("crash") == LOG_ID_CRASH) {
        dev = dev->next = new log_device_t("crash", false);
        g_devCount++;
    }
}

默认读取 main + system + crash 三个缓冲区,不是只读 main。

3.2 缓冲区名称映射

通过 android_name_to_log_id()android_log_id_to_name() 做名称与 ID 的转换:

"main"   → LOG_ID_MAIN   = 0
"radio"  → LOG_ID_RADIO  = 1
"events" → LOG_ID_EVENTS = 2
"system" → LOG_ID_SYSTEM = 3
"crash"  → LOG_ID_CRASH  = 4
// kernel 和 security 不暴露给普通 logcat

3.3 特殊值 “all” 和 “default”

// -b 解析逻辑:支持逗号/冒号/分号分隔
while ((optarg = strtok(optarg, ",:; \t\n\r\f")) != NULL) {
    if (strcmp(optarg, "default") == 0) {
        idMask |= (1 << LOG_ID_MAIN) | (1 << LOG_ID_SYSTEM) | (1 << LOG_ID_CRASH);
    } else if (strcmp(optarg, "all") == 0) {
        idMask = (unsigned)-1;   // 全部缓冲区
    } else {
        log_id_t log_id = android_name_to_log_id(optarg);
        idMask |= (1 << log_id);
    }
}

3.4 binary 标记

eventssecurity 缓冲区自动标记为 binary 模式,读取时走 android_log_processBinaryLogBuffer() 解码路径:

bool binary = !strcmp(name, "events") || !strcmp(name, "security");

3.5 常用命令

logcat                          # 默认 -b main,system,crash
logcat -b events                # 只看事件日志
logcat -b main -b system        # 同时看 main 和 system(可多次 -b)
logcat -b all                   # 所有缓冲区
logcat -b default               # main + system + crash(同默认)

四、-v 参数 — 输出格式

4.1 格式枚举

源码路径system/core/include/log/logprint.h

typedef enum {
    FORMAT_OFF = 0,
    FORMAT_BRIEF,              // 基础格式
    FORMAT_PROCESS,            // 只显示进程
    FORMAT_TAG,                // 只显示 tag
    FORMAT_THREAD,             // 显示线程
    FORMAT_RAW,                // 原始消息
    FORMAT_TIME,               // 带时间
    FORMAT_THREADTIME,         // ★ 时间 + PID + TID + 级别 + TAG(默认)
    FORMAT_LONG,               // 多行详细格式
    // 以下为修饰符,可与上述格式组合
    FORMAT_MODIFIER_COLOR,     // 按级别着色
    FORMAT_MODIFIER_TIME_USEC, // 微秒精度(默认毫秒)
    FORMAT_MODIFIER_PRINTABLE, // 非可打印字符转义
    FORMAT_MODIFIER_YEAR,      // 添加年份
    FORMAT_MODIFIER_ZONE,      // 添加时区
    FORMAT_MODIFIER_EPOCH,     // 以 Unix 时间戳显示
    FORMAT_MODIFIER_MONOTONIC, // 以启动后的时间显示
    FORMAT_MODIFIER_UID,       // 添加 UID
} AndroidLogPrintFormat;

4.2 默认格式

源码路径system/core/logcat/logcat.cpp

// 默认是 threadtime,不是 brief
setLogFormat("threadtime");

4.3 各格式示例输出

格式示例输出
briefD/MyTag(12345): hello world
processD(12345) hello world
tagD/MyTag: hello world
threadD(12345:0x3039) hello world
rawhello world
time01-15 14:30:00.123 D/MyTag(12345): hello world
threadtime01-15 14:30:00.123 12345 12345 D MyTag: hello world
long[ 01-15 14:30:00.123 12345:12345 D/MyTag ]
hello world(多行,含空行分隔)
color同 brief,但级别字符带 ANSI 颜色
usec时间戳精度为微秒(默认毫秒)
epoch1234567.890 D/MyTag(12345): hello world
monotonic从启动开始计时
printable不可打印字符显示为 \xXX

4.4 格式可以组合

# 多个修饰符可以叠加
logcat -v threadtime,color,usec,year,zone

五、过滤机制

5.1 过滤 API

源码路径system/core/include/log/logprint.h

// 添加过滤规则
int android_log_addFilterRule(AndroidLogFormat *p_format, const char *filterExpression);
int android_log_addFilterString(AndroidLogFormat *p_format, const char *filterString);

// 判断某条日志是否应该输出
int android_log_shouldPrintLine(AndroidLogFormat *p_format, const char *tag,
                                android_LogPriority pri);

logcat 不直接实现过滤逻辑,而是调用 logprint 库的上述 API。

5.2 过滤规则语法

<tag>[:priority]

例如:
  MyTag:V     →  MyTag 的日志级别 >= VERBOSE 时输出
  MyTag:D     →  MyTag 的日志级别 >= DEBUG 时输出
  *:S         →  默认静默(不输出任何未匹配的日志)
  *:V         →  默认全部输出

单独的 <tag> 等价于 <tag>:V,单独的 * 等价于 *:D

5.3 过滤规则来源(优先级从高到低)

  1. 命令行参数logcat MyTag:D *:S
  2. 环境变量 ANDROID_LOG_TAGS:命令行未指定时使用
  3. 内核 cmdline androidboot.logcat=-Q 模式专用

5.4 processBuffer() — 实际过滤与输出流程

源码路径system/core/logcat/logcat.cpp

static void processBuffer(log_device_t* dev, struct log_msg *buf)
{
    AndroidLogEntry entry;

    // 步骤1:解析日志消息
    if (dev->binary) {
        // events/security 缓冲区:用 EventTagMap 解码二进制格式
        err = android_log_processBinaryLogBuffer(&buf->entry_v1, &entry,
                                                 eventTagMap, binaryMsgBuf,
                                                 sizeof(binaryMsgBuf));
    } else {
        // 普通缓冲区:直接解析 text 格式
        err = android_log_processLogBuffer(&buf->entry_v1, &entry);
    }

    // 步骤2:过滤 — 调用 logprint 库的 API
    if (android_log_shouldPrintLine(g_logformat, entry.tag, entry.priority)) {
        // 步骤3:正则过滤(-e 参数)
        bool match = regexOk(entry);
        if (match || g_printItAnyways) {
            // 步骤4:格式化并输出 — 调用 logprint 库的 API
            android_log_printLogLine(g_logformat, g_outFD, &entry);
        }
    }

    // 步骤5:检查是否需要文件轮转(-r 参数)
    if (g_logRotateSizeKBytes > 0 && ...) {
        rotateLogs();
    }
}

关键formatBuf() 函数在 logcat.cpp 中不存在。格式化由 android_log_printLogLine() 完成,该函数内部调用 android_log_formatLogLine() 生成字符串后再写入 fd。这两个函数都定义在 system/core/liblog/logprint.c 中。


六、日志读取核心循环

6.1 liblog 读取 API

源码路径system/core/liblog/logger_read.c

logcat 通过 liblog 的 API 读取日志,不直接操作 socket:

// 1. 分配 logger_list
struct logger_list *logger_list;
if (tail_time != log_time::EPOCH) {
    logger_list = android_logger_list_alloc_time(mode, tail_time, pid);
} else {
    logger_list = android_logger_list_alloc(mode, tail_lines, pid);
}

// 2. 为每个缓冲区打开 logger
for (dev = devices; dev; dev = dev->next) {
    dev->logger = android_logger_open(logger_list,
                                      android_name_to_log_id(dev->device));
}

// 3. 主循环 — 阻塞读取
while (!g_maxCount || (g_printCount < g_maxCount)) {
    struct log_msg log_msg;
    int ret = android_logger_list_read(logger_list, &log_msg);
    // ... 处理 ...
}

6.2 log_device_t 结构

源码路径system/core/logcat/logcat.cpp

struct log_device_t {
    const char* device;       // 缓冲区名称("main", "system", ...)
    bool binary;              // 是否为二进制格式(events/security)
    struct logger *logger;    // liblog 读取句柄
    struct logger_list *logger_list;
    bool printed;             // 是否已打印过分隔线
    log_device_t* next;       // 链表指针
};

6.3 多缓冲区交替输出

当读取多个缓冲区时,logcat 在切换缓冲区时打印分隔线:

--------- beginning of main
--------- beginning of system

分隔线仅在 g_devCount > 1 时输出(-D 参数强制显示)。


七、其他重要参数

7.1 -d / -t / -T — 日志拉取模式

参数行为mode 标志
-ddump 日志后退出(非阻塞)ANDROID_LOG_RDONLY | ANDROID_LOG_NONBLOCK
-t <N>打印最近 N 行后退出(隐含 -d)同上
-t '<time>'打印指定时间之后的日志同上
-T <N>打印最近 N 行(不隐含 -d,继续等待)不设置 NONBLOCK

7.2 -e / -m — 正则过滤与计数

// -e <regex>:使用 PCRE 正则匹配日志消息内容
g_regex = new pcrecpp::RE(optarg);

// -m <count>:达到 <count> 条匹配后退出
// --print:配合 -e 和 -m,让不匹配的行也输出(但仍按匹配数计数)

7.3 -c / -g / -G — 控制命令

// -c:清除日志 — 调用 liblog API
android_logger_clear(dev->logger);

// -g:获取缓冲区大小
long size = android_logger_get_log_size(dev->logger);
long readable = android_logger_get_log_readable_size(dev->logger);
printf("%s: ring buffer is %ld%sb (%ld%sb consumed), "
       "max entry is %db, max payload is %db\n",
       dev->device, ...);

// -G <size>:设置缓冲区大小,支持 K/M/G 后缀
android_logger_set_log_size(dev->logger, setLogSize);

-g 输出示例:main: ring buffer is 256Kb (15Kb consumed), max entry is 5120b, max payload is 4069b

7.4 -L — 上次启动前的日志(pstore)

case 'L':
    mode |= ANDROID_LOG_PSTORE;
    break;

从 pstore 读取上次崩溃前的日志,而不连接 logd。

7.5 -f / -r / -n — 文件输出与轮转

// -f <file>:输出到文件(默认 stdout)
g_outputFileName = optarg;

// -r <kbytes>:每 <kbytes> KB 轮转一次(需配合 -f)
// -n <count>:最多保留 <count> 个轮转文件(默认 4)

轮转命名:file.01, file.02, ...

7.6 -B — 二进制输出

// -B:直接输出原始二进制数据,不解析
void printBinary(struct log_msg *buf) {
    size_t size = buf->len();
    TEMP_FAILURE_RETRY(write(g_outFD, buf, size));
}

7.7 -s — 静默模式

// 等同于在过滤器末尾添加 *:S
android_log_addFilterRule(g_logformat, "*:s");

7.8 --pid= — 按进程过滤

// 传递给 android_logger_list_alloc(mode, tail_lines, pid)
// 在 liblog 层按 PID 过滤

八、完整调用链(从 logcat 到 logd)

logcat main()
  │
  ├── android_logger_list_alloc(mode, tail_lines, pid)
  │     └── calloc + 初始化 logger_list 结构
  │
  ├── android_logger_open(logger_list, log_id)
  │     └── 创建 socket(PF_UNIX, SOCK_SEQPACKET)
  │         connect("/dev/socket/logdr")           ← logd 读取 socket
  │         发送 "logid <id>" + "tail <N>" 命令
  │
  └── 主循环:
        android_logger_list_read(logger_list, &log_msg)
          │
          └── recvmsg(logdr_fd)                     ← 从 logd 接收日志
              │
              ▼
        processBuffer(dev, &log_msg)
          ├── android_log_processLogBuffer()         ← 解析日志条目
          ├── android_log_shouldPrintLine()           ← 过滤检查
          ├── regexOk()                               ← 正则匹配(-e)
          └── android_log_printLogLine()              ← 格式化 + 写入 fd

logcat 通过 liblog 的 android_logger_list_read() API 从 logd 读取日志,底层通过 /dev/socket/logdr socket 通信。logd 端由 LogReader 线程响应,从 LogBuffer 中取出日志条目发送。


九、常用命令速查

# 基础用法
logcat                                      # 默认 -b main,system,crash,threadtime 格式
logcat -v time                              # 带时间戳
logcat -v threadtime                        # 默认格式(推荐)

# 缓冲区选择
logcat -b radio                             # 只看 radio
logcat -b events                            # 只看事件日志
logcat -b all                               # 所有缓冲区

# 过滤
logcat MyTag:V *:S                          # 只看 MyTag
logcat *:E                                  # 只看 Error 级别以上
logcat -s MyTag                             # 同 MyTag:V *:S
logcat -e "regex_pattern"                   # 正则匹配消息内容
logcat -m 100                               # 最多输出 100 条
logcat -e "error" -m 50 --print             # 匹配50条,但所有行都显示

# 时间/尾部
logcat -d                                   # dump 日志后退出
logcat -t 100                               # 最近 100 行
logcat -t '01-15 14:30:00.000'             # 指定时间之后
logcat -T 100                               # 最近 100 行(不退出,继续等待)

# 输出控制
logcat -c                                   # 清除日志
logcat -g                                   # 查看缓冲区大小
logcat -G 512K                              # 设置缓冲区大小为 512KB
logcat -f /sdcard/log.txt                   # 输出到文件
logcat -r 1024 -n 5                         # 轮转 5 个文件,每个 1MB
logcat -B                                   # 原始二进制输出
logcat -L                                   # 上次启动前的日志
logcat -D                                   # 显示缓冲区切换分隔线

# 格式组合
logcat -v threadtime,color,usec             # 带颜色 + 微秒精度
logcat -v epoch,uid                         # Unix时间戳 + 显示UID

# 综合示例
logcat -v threadtime -b main -b system MyApp:D *:S

十、本篇总结

  • logcat 是单文件 C++ 程序(约 1300 行),命令行解析用 getopt_long
  • 格式化与过滤逻辑在 liblog/logprint.c 中,logcat 通过 API 调用
  • 默认读取 main + system + crash 三个缓冲区,不是只读 main
  • 默认输出格式是 threadtime,不是 brief
  • 通过 android_logger_list_read() 从 logd 的 /dev/socket/logdr 读取日志
  • 格式支持 8 种基本格式 + 7 种修饰符,修饰符可以组合(如 -v threadtime,color,usec,year
  • 过滤通过 android_log_shouldPrintLine() 实现,支持 TAG:LEVEL 语法
  • 支持 PCRE 正则过滤(-e)和计数限制(-m
  • -c/-g/-G 等控制命令通过 liblog API 发送到 logd
  • -t-T 的区别:-t 隐含 -d(读完后退出),-T 不退出

下一篇将分析日志缓冲区的容量管理、裁剪与统计机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值