我们做Android开发,几乎每天都会写一句:Log.d
,但是你真的清楚日志系统是如何工作的吗?我们写着一句之后,他经历了什么,才能最终在屏幕上print出你要的日志内容的吗? 今天跟着小马哥一起来看看Android日志系统
一、开局一张图
1、关键类
logd
简介
- 守护进程,开机时由 init 进程;logd 内部维护了一个 RAM buffer 用作日志的缓存。各个进程的日志都会写入此缓存中。如果日志大小超过缓存限定,就会删除最老(根据一系列规则)的日志。
LogBuffer
LogReader
LogListener
CommandListener
二、源码解析
1、logd启动
- logd 是经由 init 进程启动的(init启动系统框架层的各种服务)
- logd.rc启动服务
- 创建了 3 个(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中的log
- LogListener监听/dev/socket/logdw,有日志写入的时候,数据写入LogBuff
- CommandListener监听/dev/socket/logd,负责监听给logd的指令并处理
如果配置了ro.logd.auditd 为ture会启动LogAudit,主要负责selinux相关的日志
如果配置了ro.logd.kernel 为true会启动LogKlog,主要负责收集内核相关的日志
全部继承于SocketListener(system/core/libsysutils)
3、log写入
less
# 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);
}
Android层调用Log/Slog/Rlog中的v/d方法打印log,最终会调用到
system/core/liblog/logger_write.c 中的__android_log_buf_write函数
文件最终写到 "/dev/socket/logdw"中,此时logd中的LogListener会监测到有log需写入
arduino
static int write_to_log(log_id_t log_id, struct iovec* vec, size_t nr) {
int ret;
ret = LogdWrite(log_id, &ts, vec, nr);
PmsgWrite(log_id, &ts, vec, nr);
return ret;
}
arduino
static int logdOpen() {
strcpy(un.sun_path, "/dev/socket/logdw");
}
- LogdWrite是往socket /dev/socket/logdw 中写入数据,这个socket在创建logd进程时候被创建,并在初始化之后由LogListener监听
- PmsgWrite是往 /dev/pmsg0 中写
LogListener
当有对端进程通过Socket传递过来数据后,onDataAvailable方法被调用,
- 调用LogBuffer->log方法存储日志信息,将消息存在LogBuffer中
- 当写入 log 数据后,由于此时可能有客户端在等待读取数据,所以需要唤醒他们(LogReader)
4、LogBuffer
4.1 初始化
设置缓冲区大小
scss
void LogBuffer::init() {
log_id_for_each(i) {
mLastSet[i] = false;
mLast[i] = mLogElements.begin();
if (setSize(i, __android_logger_get_buffer_size(i))) {
setSize(i, LOG_BUFFER_MIN_SIZE);
}
}
// ...
}
- 创建LogBuffer对象,在classLogBuffer类中,定义了一个list容器,保存了指向LogBufferElement对象的指针,创建LogBuffer对象时在其构造函数中会调用LogBuffer::init()函数初始化各log域(如main/system/kernel/crash等)的大小。
4.2 log方法写入
比对新进日志信息的时间,将其插入到正确的存储位置。所有日志存储在mLogElements变量中,其类型是:typedef std::list<LogBufferElement*>
- 删除过多的 log(maybePrune方法)
他主要完成下面几件事:
- 计算一个 时间,表示所有客户正在读取的最早的log。时间小于 此时间的 log 都不能删除
- 如果是客户请求删除 log,删除对应 uid 的 log
- 删除黑名单里的 log
- 如果已删除的条数还不够,删除不在白名单里的 log
- 如果已删除的条数还不够,删除白名单里的 log
5、log读取
初始化监听对象,开始监听Socket
scss
LogReader* reader = new LogReader(logBuf);
if (reader->startListener()) {
exit(1);
}
logdReadr函数负责打开/dev/logdr,并通过socket获取log。
监听客户端连接
LogReader同样是SocketListener的子类,也是在onDataAvailable中处理连接数据
当有客户端连接的时候,SocketListener
会回调子类的 onDataAvailable
函数。在这个函数中,LogReader
主要做 3 件事:
- 设置线程名
- 读取客户端传过来的参数
- 生成一个
FlushCommand
用于向客户端写回 log 数据。
启动读日志线程
- 调用LogBuffer----flushTo获取日志
reader->sendDatav
写入 客户端socket
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)
scss
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
输出当前缓冲区日志。
(1)命令格式
adb logcat [选项] [过滤项]
注:[]中的内容表示是可选的,所以选项和过滤项是可选的。
参数 描述
-b 加载一个可使用的日志缓冲区供查看,比如event和radio。默认值是main
-g 打印日志缓冲区的大小并退出
-c 清除缓冲区中的全部日志并退出(清除完后可以使用-g查看缓冲区)
-d 将缓冲区的log转存到屏幕中然后退出, 不阻塞
-t 输出最近的count行日志, 输出完退出, 不阻塞
-B 以二进制形式输出日志内容
-s 设置输出日志的标签, 只显示该标签的日志
-f 将log输出到指定的文件中,默认为标准输出(stdout)
-r 按照每千字节输出日志,默认值是16,需要和-f选项一起使用
-n 设置日志输出的最大数目,默认值是4,需要和-r选项一起使用
-v 设置日志的输出格式,注意只能设置一项