我们做Android开发,几乎每天都会写一句:Log.d,但是你真的清楚日志系统是如何工作的吗?我们写着一句之后,他经历了什么,才能最终在屏幕上print出你要的日志内容的吗?
今天跟着小马哥一起来看看Android日志系统
一、流程梳理
首先看看下面这个流程图
1、核心元素
logd
简介
守护进程,开机时由 init 进程;logd 内部维护了一个 RAM buffer 用作日志的缓存。各个进程的日志都会写入此缓存中。如果日志大小超过缓存限定,就会删除最老(根据一系列规则)的日志。
二、源码解析
1、logd启动
logd是通过init进程启动的,init负责启动系统框架层的各种服务
-
logd.rc启动了服务 -
创建了三个 UNIX 域 socket:
/dev/socket/logd,/dev/socket/logdr,/dev/socket/logdw -
打开了两个文件:
/proc/kmsg,/dev/kmsg -
将
logd的 UID 设置为logd,GID 设置为logd、system、package_info和readproc -
将
logd进程的 PID 写入文件/dev/cpuset/system-background/tasks
2、初始化
在 main.cpp 的 main 函数中:
- 打开
/dev/kmsg文件以获取文件描述符(fd)。 - 根据系统属性
ro.logd.kernel的设置,如果为true,则打开/proc/kmsg文件以获取文件描述符(fd)。 - 创建
LogBuffer对象用于存储日志。 LogReader监听/dev/socket/logdr,当有客户端连接时,从LogBuffer中读取日志。LogListener监听/dev/socket/logdw,当有日志写入时,将数据写入LogBuffer。CommandListener监听/dev/socket/logd,负责接收并处理发送给logd的指令。
如果系统配置了 ro.logd.auditd 为 true,将启动 LogAudit,主要负责处理与 SELinux 相关的日志。
如果系统配置了 ro.logd.kernel 为 true,将启动 LogKlog,主要负责收集与内核相关的日志。
所有组件都继承自 SocketListener(位于 system/core/libsysutils)。
3、log写入
Android 层的日志打印方法(如 Log.v())最终会调用到系统层的 __android_log_buf_write 函数,该函数位于 system/core/liblog/logger_write.c 文件中。
这里是具体的流程:
-
Android层日志打印方法调用: 在 Android 框架中,调用类似
Log.v(tag, msg)的方法时,会最终调用到println_native方法,它通过 JNI 调用到底层的__android_log_buf_write函数。javaCopy Code // framework/base/core/java/android/util/Log.java public static int v(@Nullable String tag, @NonNull String msg) { return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); } -
底层日志写入函数: 在
system/core/liblog/logger_write.c文件中,定义了__android_log_buf_write函数,它实现了将日志写入操作。cCopy Code static int __android_log_buf_write(log_id_t log_id, struct iovec* vec, size_t nr) { // 实际的写入操作,例如 LogdWrite 和 PmsgWrite return write_to_log(log_id, vec, nr); } -
具体写入操作: 在
write_to_log函数中,将调用LogdWrite和PmsgWrite来完成具体的日志写入操作。cCopy Code static int write_to_log(log_id_t log_id, struct iovec* vec, size_t nr) { int ret; // 向 logd 写入日志 ret = LogdWrite(log_id, vec, nr); // 向 pmsg0 写入日志 PmsgWrite(log_id, vec, nr); return ret; } -
LogdWrite 和 PmsgWrite 函数:
LogdWrite函数负责将日志写入/dev/socket/logdw这个 Unix 域 socket 中。该 socket 在logd进程创建和初始化之后被监听,由LogListener负责处理其中的日志写入事件。PmsgWrite函数负责将日志写入/dev/pmsg0。
-
LogListener 监听:
LogListener是logd进程中的组件,负责监听/dev/socket/logdwsocket,一旦有日志写入,就会相应地处理这些日志数据。
总结来说,Android 层的日志打印最终通过底层的 __android_log_buf_write 函数写入到 /dev/socket/logdw,由 LogListener 监听并处理。
LogListener
当对端进程通过Socket传递数据时,触发 onDataAvailable 方法:
LogBuffer->log方法被调用,将日志信息存储在LogBuffer中。- 完成日志数据的写入后,由于可能有等待读取数据的客户端(
LogReader),需要唤醒它们。
4、LogBuffer
日志系统的初始化经历了以下步骤
4.1 初始化
首先是初始化,以下是初始化相关代码:
cppCopy Code
void LogBuffer::init() {
// 初始化日志缓冲区大小
log_id_for_each(i) {
mLastSet[i] = false;
mLast[i] = mLogElements.begin();
// 获取默认的缓冲区大小
size_t bufferSize = __android_logger_get_buffer_size(i);
// 如果设置缓冲区大小失败,则使用最小的日志缓冲区大小
if (!setSize(i, bufferSize)) {
setSize(i, LOG_BUFFER_MIN_SIZE);
}
}
// ...
}
4.2 日志写入方法
在 log 方法中,根据新进日志的时间戳,将其插入到适当的存储位置。所有的日志都存储在 mLogElements 变量中,类型为 std::list<LogBufferElement*>。
4.3 删除过多的日志(maybePrune 方法)
maybePrune 方法执行以下操作:
- 计算一个时间戳,表示所有客户端当前正在读取的最早日志的时间。早于这个时间戳的日志不能删除。
- 如果有客户端请求删除特定 UID 的日志,则执行删除操作。
- 删除黑名单中的日志条目。
- 如果已删除的日志条目数量不足,继续删除不在白名单中的日志。
- 如果仍未删除足够数量的日志条目,删除白名单中的日志。
5. 日志读取
初始化日志监听器对象,并开始监听 Socket:
cppCopy Code
LogReader* reader = new LogReader(logBuf);
if (reader->startListener()) {
exit(1);
}
LogReader 类的 startListener 方法负责打开 /dev/logdr,通过 Socket 获取日志数据。
5.1 监听客户端连接
LogReader同样是SocketListener的子类,也是在onDataAvailable中处理连接数据
当有客户端连接的时候,SocketListener 会回调子类的 onDataAvailable 函数。在这个函数中,LogReader 主要做 3 件事:
- 设置线程名
- 读取客户端传过来的参数
- 生成一个
FlushCommand用于向客户端写回 log 数据。
5.2 启动读日志线程
- 调用
LogBuffer的flushTo方法来获取日志。 - 将日志写入客户端 socket 使用
reader->sendData。
5.3 SocketListener
SocketListener 是 sysutils 库提供的类,用于监听 socket。当有数据可读时,SocketListener 会调用子类的 onDataAvailable 方法。
在初始化 Listener 后,main 函数调用 startListener(600) 开始监听客户端请求。
三、logcat
1、启动
logcat 进程的启动入口在 logcat_main.cpp 文件的 main() 方法中。
通过 android_logcat_run_command 方法来执行命令,该方法最终调用 __logcat 来解析命令参数,并通过 socket 发送给 logd CommandListener 类。
2、读取
最终调用读取日志的 logger_readr 方法:
int ret = android_logger_list_read(logger_list.get(), &log_msg);
int android_logger_list_read(struct logger_list* logger_list, struct log_msg* log_msg)
{
if (logger_list->mode & ANDROID_LOG_PSTORE) {
ret = PmsgRead(logger_list, log_msg);
} else {
ret = LogdRead(logger_list, log_msg);
}
}
logcat 最终打开 /dev/socket/logdr,并通过 socket 传输数据进行处理。
四、日志管理
1、日志等级:
- V:详细(最低优先级)-- (Verbose:2)
- D:调试 -- (Debug:3)
- I:信息 -- (Info:4)
- W:警告 -- (Warning:5)
- E:错误 -- (Error:6)
- F:严重错误 -- (Fatal:7)
- S:静默(最高优先级,绝不会输出任何内容)-- (Silent)
2、缓冲区
- radio:用于查看包含无线设备/电话相关消息的缓冲区。
- events:用于查看已解析的二进制系统事件缓冲区消息。
- main:主日志缓冲区(默认),不包含系统和崩溃日志消息。
- system:系统日志缓冲区(默认)。
- crash:崩溃日志缓冲区(默认)。
3,日志的存储
4、日志格式
- brief:显示优先级、标记以及发出消息的进程的 PID。
- long:显示所有元数据字段,并使用空白行分隔消息。
- process:仅显示 PID。
- raw:显示不包含其他元数据字段的原始日志消息。
- tag:仅显示优先级和标记。
- thread::旧版格式,显示优先级、PID 以及发出消息的线程的 TID。
- threadtime(默认值):显示日期、调用时间、优先级、标记、PID 以及发出消息的线程的 TID。
- time:显示日期、调用时间、优先级、标记以及发出消息的进程的 PID。
5、日志指令
adb logcat / adb shell logcat
显示所选缓冲区的日志。
命令格式
adb logcat [选项] [过滤项]
注:方括号中的内容为可选,包括选项和过滤项。
参数说明
- -b : 指定要查看的日志缓冲区(如:event、radio)。默认为 'main'。
- -g: 打印日志缓冲区的大小并退出。
- -c: 清除整个日志缓冲区并退出(使用
-g可查看清除后的缓冲区大小)。 - -d: 将日志缓冲区转储到屏幕并退出,非阻塞。
- -t : 输出最近的
count行日志,然后退出,非阻塞。 - -B: 以二进制格式输出日志内容。
- -s : 仅显示指定标签的日志。
- -f : 将日志输出到指定文件,默认为标准输出(stdout)。
- -r : 设置日志输出的每轮大小(默认为16 KB),需与
-f选项一起使用。 - -n : 设置日志输出的最大轮数(默认为4),需与
-r选项一起使用。 - -v : 设置日志的输出格式,只能设置一种格式。
这个描述清晰简洁地概述了每个 adb logcat 的选项和参数,确保用户能够轻松理解和使用。