当前位置: 首页 > news >正文

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不退出

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

http://www.cnnetsun.cn/news/3112088.html

相关文章:

  • AI科研效率革命:用Claude技能包重构论文写作与数据分析流程
  • 海外短剧平台技术架构与运营实战指南
  • 本地部署AI Agent,6G显存跑Qwen3.6-35B-A3B 从入门到实战全流程
  • 科技融匠心!康姿百德学生床垫筑牢成长睡眠防线
  • 嵌套 H5 的跨端通信:iOS / Android / 小程序 / 浏览器
  • 第【48期】-- 通信问题的cvx教程之基础篇【一】-- MU-MIMO下行功率分配问题
  • Node.js Promise.all 并行查询实战:性能提升与错误处理详解
  • RAG 是什么?让大模型读懂私有知识库的关键技术
  • 多项式回归实战:用3阶曲线拟合替代线性模型
  • 180火龙传奇打金搬砖三天测试表:新手怎么判断有没有跑顺
  • tModCodeAssist:泰拉瑞亚模组开发者的智能代码助手终极指南
  • KWM转MP3:从酷我加密容器到通用格式,5种技术方案完全解析
  • AzurLaneAutoScript:碧蓝航线自动化脚本的最佳实践与技术架构解析
  • Normal Equation实战指南:线性回归闭式解的稳定实现与工程落地
  • 从代码到参数:2026年AI前沿技术深度拆解
  • 铁客流智能监控:YOLOv8姿态识别数据集全解析,从训练到部署实战指南10761期
  • 电商运营Agent
  • 微软在2002年推出了第一个版本的 .NET Framework,这是一个主要面向Windows 桌面(Windows Forms)和服务器(ASP.NET Web Forms)的基础框架。在此之后,
  • 鼓浪屿:鹭江之上的琴音与时光
  • 【Java课程设计/毕业设计】基于 SpringBoot 的课程评分分析与智能推荐平台的设计与实现 智慧校园个性化教学资源服务推荐系统【附源码、数据库、万字文档】
  • 让大模型拿到实时搜索结果:SERP API 的一个实现方案
  • 智能动效检查:AI 可以看节奏,但标准要由人定义
  • 投机解码技术解析:如何用DSpark实现大模型推理85%加速
  • ASP.NET 8 Cookie身份验证实现与安全实践
  • MetaTube插件:Jellyfin/Emby媒体库的终极元数据自动刮削解决方案
  • 学习型查询优化器:特征漂移比模型精度更麻烦
  • 2026手机抠图工具实操指南:人像物品背景去除,安卓苹果免费软件整理
  • GraphQL 成本控制:灵活查询也要有防火墙
  • MySQL 慢查询根治指南:从 EXPLAIN 看懂到索引覆盖率优化的完整链路
  • AI 后端队列背压:请求堆住时,系统要会说不