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

我们做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 件事:

  1. 设置线程名
  2. 读取客户端传过来的参数
  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 设置日志的输出格式,注意只能设置一项

相关推荐
Abelard_6 分钟前
LeetCode--347.前k个高频元素(使用优先队列解决)
java·算法·leetcode
海海不掉头发23 分钟前
软件工程-【软件项目管理】--期末复习题汇总
java·学习·产品运营·软件工程·团队开发·需求分析·期末复习
吴敬悦25 分钟前
领导:按规范提交代码conventionalcommit
前端·程序员·前端工程化
缘友一世25 分钟前
java实现网络IO高并发编程java AIO
java·网络·python
洞见不一样的自己27 分钟前
android 常用方法
android
CodeClimb29 分钟前
【华为OD-E卷 - 猜字谜100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
暗碳33 分钟前
华为麦芒5(安卓6)termux记录 使用ddns-go,alist
android·linux
ADRU36 分钟前
设计模式-责任链模式
java·设计模式·责任链模式
heeheeai37 分钟前
kotlin 函数作为参数
java·算法·kotlin
吴冰_hogan1 小时前
Java虚拟机(JVM)的类加载器与双亲委派机制
java·开发语言·jvm