Android日志是如何输出的:Log.d经历了什么

1,955 阅读8分钟

我们做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 设置为 logdsystempackage_inforeadproc

  • logd 进程的 PID 写入文件 /dev/cpuset/system-background/tasks

2、初始化

main.cppmain 函数中:

  • 打开 /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.auditdtrue,将启动 LogAudit,主要负责处理与 SELinux 相关的日志。

如果系统配置了 ro.logd.kerneltrue,将启动 LogKlog,主要负责收集与内核相关的日志。

所有组件都继承自 SocketListener(位于 system/core/libsysutils)。

3、log写入

Android 层的日志打印方法(如 Log.v())最终会调用到系统层的 __android_log_buf_write 函数,该函数位于 system/core/liblog/logger_write.c 文件中。

这里是具体的流程:

  1. 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);
    }
    
  2. 底层日志写入函数: 在 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);
    }
    
  3. 具体写入操作: 在 write_to_log 函数中,将调用 LogdWritePmsgWrite 来完成具体的日志写入操作。

    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;
    }
    
  4. LogdWrite 和 PmsgWrite 函数

    • LogdWrite 函数负责将日志写入 /dev/socket/logdw 这个 Unix 域 socket 中。该 socket 在 logd 进程创建和初始化之后被监听,由 LogListener 负责处理其中的日志写入事件。
    • PmsgWrite 函数负责将日志写入 /dev/pmsg0
  5. LogListener 监听LogListenerlogd 进程中的组件,负责监听 /dev/socket/logdw socket,一旦有日志写入,就会相应地处理这些日志数据。

总结来说,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 件事:

  1. 设置线程名
  2. 读取客户端传过来的参数
  3. 生成一个 FlushCommand 用于向客户端写回 log 数据。

5.2 启动读日志线程

image.png

  • 调用 LogBuffer 的 flushTo 方法来获取日志。
  • 将日志写入客户端 socket 使用 reader->sendData

5.3 SocketListener

SocketListenersysutils 库提供的类,用于监听 socket。当有数据可读时,SocketListener 会调用子类的 onDataAvailable 方法。

在初始化 Listener 后,main 函数调用 startListener(600) 开始监听客户端请求。

三、logcat

1、启动

logcat 进程的启动入口在 logcat_main.cpp 文件的 main() 方法中。

通过 android_logcat_run_command 方法来执行命令,该方法最终调用 __logcat 来解析命令参数,并通过 socket 发送给 logd CommandListener 类。

image.png

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);      
        }
    }

image.png

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)

image.png

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 的选项和参数,确保用户能够轻松理解和使用。