news 2026/7/3 2:54:28

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 7系统日志(五)日志读取—logcat源码深度分析

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


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

一、logcat 源码概览

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

涉及的关键库文件:

system/core/liblog/logprint.c ← 格式化和过滤逻辑 system/core/liblog/logger_read.c ← 日志读取 API(android_logger_list_read) 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() 入口 — 三层结构

intmain(intargc,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 默认缓冲区

// 未指定 -b 时的默认值(logcat.cpp 第1035-1046行)if(!devices){dev=devices=newlog_device_t("main",false);g_devCount=1;if(android_name_to_log_id("system")==LOG_ID_SYSTEM){dev=dev->next=newlog_device_t("system",false);g_devCount++;}if(android_name_to_log_id("crash")==LOG_ID_CRASH){dev=dev->next=newlog_device_t("crash",false);g_devCount++;}}

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

3.2 缓冲区名称映射

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

// liblog 内部映射(logger_write.c 中定义)"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 解析逻辑(第843-861行)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);}elseif(strcmp(optarg,"all")==0){idMask=(unsigned)-1;// 全部缓冲区}else{log_id_t log_id=android_name_to_log_id(optarg);idMask|=(1<<log_id);}}

支持逗号/冒号/分号分隔:-b main,system-b main -b system等效。

3.4 binary 标记

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

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

3.5 常用命令

logcat# 默认 -b main,system,crashlogcat-bevents# 只看事件日志logcat-bmain-bsystem# 同时看 main 和 system(可多次 -b)logcat-ball# 所有缓冲区logcat-bdefault# main + system + crash(同默认)

四、-v 参数 — 输出格式

4.1 格式枚举(logprint.h)

typedefenum{FORMAT_OFF=0,FORMAT_BRIEF,// 基础格式FORMAT_PROCESS,// 只显示进程FORMAT_TAG,// 只显示 tagFORMAT_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 默认格式

// 源码第1064行:默认是 threadtime,不是 briefsetLogFormat("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-vthreadtime,color,usec,year,zone

五、过滤机制

5.1 过滤 API(logprint.h / logprint.c)

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

// 添加过滤规则intandroid_log_addFilterRule(AndroidLogFormat*p_format,constchar*filterExpression);intandroid_log_addFilterString(AndroidLogFormat*p_format,constchar*filterString);// 判断某条日志是否应该输出intandroid_log_shouldPrintLine(AndroidLogFormat*p_format,constchar*tag,android_LogPriority pri);

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. 内核 cmdlineandroidboot.logcat=-Q模式专用

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

staticvoidprocessBuffer(log_device_t*dev,structlog_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 库的 APIif(android_log_shouldPrintLine(g_logformat,entry.tag,entry.priority)){// 步骤3:正则过滤(-e 参数)boolmatch=regexOk(entry);g_printCount+=match;if(match||g_printItAnyways){// 步骤4:格式化并输出 — 调用 logprint 库的 APIbytesWritten=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。


六、日志读取核心循环

6.1 liblog 读取 API

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

// 1. 分配 logger_liststructlogger_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. 为每个缓冲区打开 loggerfor(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)){structlog_msglog_msg;intret=android_logger_list_read(logger_list,&log_msg);// ... 处理 ...}

6.2 log_device_t 结构

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

6.3 多缓冲区交替输出

当读取多个缓冲区时,logcat 在切换缓冲区时打印分隔线(-D参数强制显示):

--------- 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=newpcrecpp::RE(optarg);// -m <count>:达到 <count> 条匹配后退出// --print:配合 -e 和 -m,让不匹配的行也输出(但仍按匹配数计数)

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

// -c:清除日志if(g_outputFileName){// 输出到文件时:删除文件unlink(file);}else{// 正常模式:调用 liblog APIandroid_logger_clear(dev->logger);}// -g:获取缓冲区大小longsize=android_logger_get_log_size(dev->logger);longreadable=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,value_of_size(size),multiplier_of_size(size),value_of_size(readable),multiplier_of_size(readable),(int)LOGGER_ENTRY_MAX_LEN,(int)LOGGER_ENTRY_MAX_PAYLOAD);// -G <size>:设置缓冲区大小// 支持 K/M/G 后缀,如 -G 256K, -G 1Mandroid_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)g_logRotateSizeKBytes=...// -n <count>:最多保留 <count> 个轮转文件(默认 4)g_maxRotatedLogs=DEFAULT_MAX_ROTATED_LOGS;// 4

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

7.6 -B — 二进制输出

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

7.7 -s — 静默模式

case's':// 等同于在过滤器末尾添加 *:Sandroid_log_addFilterRule(g_logformat,"*:s");break;

7.8 --pid= — 按进程过滤

if(long_options[option_index].name==pid_str){getSizeTArg(optarg,&pid,1);}// 传递给 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/logdrsocket 通信。logd 端由LogReader线程响应,从LogBuffer中取出日志条目发送。


九、常用命令速查

# 基础用法logcat# 默认 -b main,system,crash,threadtime 格式logcat-vtime# 带时间戳logcat-vthreadtime# 默认格式(推荐)# 缓冲区选择logcat-bradio# 只看 radiologcat-bevents# 只看事件日志logcat-ball# 所有缓冲区# 过滤logcat MyTag:V *:S# 只看 MyTaglogcat *:E# 只看 Error 级别以上logcat-sMyTag# 同 MyTag:V *:Slogcat-e"regex_pattern"# 正则匹配消息内容logcat-m100# 最多输出 100 条logcat-e"error"-m50--print# 匹配50条,但所有行都显示# 时间/尾部logcat-d# dump 日志后退出logcat-t100# 最近 100 行logcat-t'01-15 14:30:00.000'# 指定时间之后logcat-T100# 最近 100 行(不退出,继续等待)# 输出控制logcat-c# 清除日志logcat-g# 查看缓冲区大小logcat-G512K# 设置缓冲区大小为 512KBlogcat-f/sdcard/log.txt# 输出到文件logcat-r1024-n5# 轮转 5 个文件,每个 1MBlogcat-B# 原始二进制输出logcat-L# 上次启动前的日志logcat-D# 显示缓冲区切换分隔线# 格式组合logcat-vthreadtime,color,usec# 带颜色 + 微秒精度logcat-vepoch,uid# Unix时间戳 + 显示UID# 综合示例logcat-vthreadtime-bmain-bsystem MyApp:D *:S

十、本篇总结

  • logcat 是单文件 C++ 程序(1327 行),命令行解析用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不退出

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 2:45:06

AI科研效率革命:用Claude技能包重构论文写作与数据分析流程

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Claude 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 如果你还在用“帮我写论文”这种模糊指令来使用 Claude 或 Codex&#xff0c;那你可能只发挥了它们 10% 的潜力。真正的效率革命&am…

作者头像 李华
网站建设 2026/7/3 2:44:16

海外短剧平台技术架构与运营实战指南

1. 项目背景与市场需求最近两年&#xff0c;海外短剧市场呈现爆发式增长。根据行业数据显示&#xff0c;2023年全球短剧市场规模已突破50亿美元&#xff0c;年增长率超过300%。这种单集时长3-10分钟、剧情紧凑的短视频内容形式&#xff0c;正在成为内容创业的新风口。我去年为一…

作者头像 李华
网站建设 2026/7/3 2:43:48

本地部署AI Agent,6G显存跑Qwen3.6-35B-A3B 从入门到实战全流程

&#x1f4a1; 读完这篇&#xff0c;你能做到这三件事&#xff1a; 1️⃣ 用 6G 显存跑 35B 超大模型&#xff08;MoE 架构的黑魔法&#xff09; 2️⃣ 把你的 Windows 电脑变成一台完全不花钱、不联网、无限 token 的本地 AI 服务器 3️⃣ 接入 Hermes Agent&#xff0c;打造真…

作者头像 李华
网站建设 2026/7/3 2:43:04

科技融匠心!康姿百德学生床垫筑牢成长睡眠防线

青少年脊柱发育关键期&#xff01;康姿百德学生床垫护脊防驼背苏晴的儿子小宇刚上初一&#xff0c;正是身高猛增、脊柱发育的关键期。可最近小宇总说晨起腰酸背痛&#xff0c;写作业时还不自觉地驼背&#xff0c;这可急坏了苏晴。她排查后发现&#xff0c;问题出在那张普通儿童…

作者头像 李华
网站建设 2026/7/3 2:41:24

嵌套 H5 的跨端通信:iOS / Android / 小程序 / 浏览器

一、为什么要做“统一桥接层”&#xff1f; “Write once, run anywhere” 对于纯展示型 H5 是成立的。但只要涉及到业务交互&#xff0c;比如&#xff1a;调起原生登录、保存图片到相册、修改系统状态栏颜色、分享到朋友圈&#xff0c;浏览器标准的 Web API 根本无能为力。 …

作者头像 李华