• Android Logger日志系统


    前言
    该篇文章是我的读书和实践笔记。参考的是《Android系统源代码情景分析》。

    运行时库层日志库——liblog
    Android系统在运行时库层提供了一个用来和Logger日志驱动程序进行交互的日志库liblog。通过日志库liblog提供的接口,应用程序就可以方便地往Logger日志驱动程序中写入日志记录。

    位于运行时库层的C/C++日志写入接口和位于应用程序框架层的Java日志写入接口都是通过liblog库提供的日志写入接口来往Logger日志驱动程序中写入日志记录的。

    源码分析
    日志库liblog提供的日志记录写入接口实现在logd_write.c文件中,它的源码位置为:/system/core/liblog/logd_write.c。

    根据写入的日志记录的类型不同,这些函数可以划分为三个类别,其中:

    函数__android_log_assert、__android_log_vprint和__android_log_print用来写入类型为main的日志记录。
    函数__android_log_btwrite和__android_log_bwrite用来写入类型为events的日志记录。
    函数__android_log_buf_print可以写入任意一种类型的日志记录。
    无论写入的是什么类型的日志记录,它们最终都是通过调用函数write_to_log写入到Logger日志驱动程序中的。write_to_log是一个函数指针,它开始时指向函数__write_to_log_init。因此,当函数write_to_log第一次被调用时,实际上执行的是函数__write_to_log_init。函数__write_to_log_init主要是进行一些日志库初始化操作,接着函数指针write_to_log重定向到函数__write_to_log_kernel或者__write_to_log_null中,这取决于能否成功地将日志设备文件打开。

    源码分析如上,源码实现如下:

    // 先声明,后引用
    static int __write_to_log_init(log_id_t, struct iovec *vec, size_t nr);
    int (*write_to_log)(log_id_t, struct iovec *vec, size_t nr) = __write_to_log_init;
    
    // 一些定义在system/core/include/cutils/log.h中的宏
    typedef enum {
    LOG_ID_MAIN = 0,
    LOG_ID_RADIO = 1,
    LOG_ID_EVENTS = 2,
    LOG_ID_SYSTEM = 3,
    
    LOG_ID_MAX
    } log_id_t;
    
    #define LOGGER_LOG_MAIN "log/main"
    #define LOGGER_LOG_RADIO "log/radio"
    #define LOGGER_LOG_EVENTS "log/events"
    #define LOGGER_LOG_SYSTEM "log/system"
    
    // 真正函数执行的地方
    static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr)
    {
    if (write_to_log == __write_to_log_init) {
    log_fds[LOG_ID_MAIN] = log_open("/dev/"LOGGER_LOG_MAIN, O_WRONLY);
    log_fds[LOG_ID_RADIO] = log_open("/dev/"LOGGER_LOG_RADIO, O_WRONLY);
    log_fds[LOG_ID_EVENTS] = log_open("/dev/"LOGGER_LOG_EVENTS, O_WRONLY);
    log_fds[LOG_ID_SYSTEM] = log_open("/dev/"LOGGER_LOG_SYSTEM, O_WRONLY);
    
    // 修改write_to_log函数指针
    write_to_log = __write_to_log_kernel;
    
    if (log_fds[LOG_ID_MAIN] < 0 || log_fds[LOG_ID_RADIO] < 0 || log_fds[LOG_ID_EVENTS] < 0) {
    log_close(log_fds[LOG_ID_MAIN]);
    log_close(log_fds[LOG_ID_RADIO]);
    log_close(log_fds[LOG_ID_EVENTS]);
    log_fds[LOG_ID_MAIN] = -1;
    log_fds[LOG_ID_RADIO] = -1;
    log_fds[LOG_ID_EVENTS] = -1;
    write_to_log = __write_to_log_null;
    }
    
    if (log_fds[LOG_ID_SYSTEM] < 0) {
    log_fds[LOG_ID_SYSTEM] = log_fds[LOG_ID_MAIN];
    }
    }
    
    return write_to_log(log_id, vec, nr);
    }

    通过上述代码,我们在替换宏定义之后,是可以知道调用log_open打开的分别是/dev/log/main、/dev/log/radio、/dev/log/events、/dev/log/system四个日志设备文件。而宏log_open定义在system/core/liblog/logd_write.c中:

    #if FAKE_LOG_DEVICE
    // 不需要care这里,真正编译的时候FAKE_LOG_DEVICE为0
    #else
    #define log_open(pathname, flags) open(pathname, (flags) | O_CLOEXEC)
    #define log_writev(filedes, vector, count) writev(filedes, vector, count)
    #define log_close(filedes) close(filedes)
    #endif

    从上面代码可以看出,log_open的真正实现是open函数。

    回到最开始的地方,如果log_open的文件都是ok的,那接下来会调用__write_to_log_kernel函数,源码实现如下:

    static int __write_to_log_kernel(log_id_t log_id, struct iovec *vec, size_t nr)
    {
    ssize_t ret;
    int log_fd;
    
    if ((int)log_id < (int)LOG_ID_MAX) {
    log_fd = log_fds[(int)log_id];
    } else {
    return EBADF;
    }
    
    do {
    ret = log_writev(log_fd, vec, nr);
    } while (ret < 0 && errno == EINTR);
    
    return ret;
    }

    函数__write_to_log_kernel会根据参数log_id在全局数组log_fds中找到对应的日志设备文件描述符,然后调用宏log_writev,即函数writev,把日志记录写入到Logger日志驱动程序中。

    如果设备文件打开失败的话,write_to_log函数指针会被赋值为__write_to_log_kernel,这个函数其实什么都没有做,只是返回了个-1。所以就不贴源码了。

    最后,我们在分析一下__android_log_buf_write函数。因为C/C++日志写入接口和Java日志写入接口最终都是调用了这个函数完成了日志的写入。源码如下:

    int __android_log_buf_write(int bufID, int prio, const char *tag, const char *msg)
    {
    struct iovec vec[3];
    char tmp_tag[32];
    
    if (! tag) tag = "";
    
    if ((bufID != LOG_ID_RADIO) &&
    (!strcmp(tag, "HTC_RIL") ||
    (!strncmp(tag, "RIL", 3)) ||
    (!strncmp(tag, "IMS", 3)) ||
    !strcmp(tag, "AT") ||
    !strcmp(tag, "GSM") ||
    !strcmp(tag, "STK") ||
    !strcmp(tag, "CDMA") ||
    !strcmp(tag, "PHONE") ||
    !strcmp(tag, "SMS"))) {
    bufID = LOG_ID_RADIO;
    snprintf(tmp_tag, sizeof(tmp_tag), "use-Rlog/RLOG-%s", tag);
    tag = tmp_tag; 
    }
    
    vec[0].iov_base = (unsigned char *) &prio;
    vec[0].iov_len = 1;
    vec[1].iov_base = (void *) tag;
    vec[1].iov_len = strlen(tag) + 1;
    vec[2].iov_base = (void *) msg;
    vec[2].iov_len = strlen(msg) + 1;
    
    return write_to_log(log_id, vec, 3); 
    }

    在默认情况下,函数__android_log_write写入的日志记录类型为main。然后,如果传进来的日志记录的标请以”RIL”等标志开头,那么它就会被认为是类型是radio的日志记录。

    C/C++日志写入接口
    Android系统提供了三组常用的C/C++宏来封装日志写入接口。之所以这样做,是为了方便开发同学进行日志的开关控制,例如不在发布版本中打开日志。

    三组宏定义分别为:

    ALOGV,ALOGD,ALOGI,ALOGW和ALOGE。用来记录类型为main的日志。
    SLOGV,SLOGD,SLOGI,SLOGW和SLOGE,用来写入类型为system的日志。
    LOG_EVENT_INT,LOG_EVENT_LONG和LOG_EVENT_STRING,它们用来写入类型为events的日志记录。
    这些宏定义在system/core/include/log/log.h中,并且使用了一个LOG_NDEBUG的宏来作为日志开关。

    具体源码如下:

    // 日志开关
    #ifndef LOG_NDEBUG
    #ifdef NDEBUG
    #define LOG_NDEBUG 1
    #else
    #define LOG_NDEBUG 0
    #endif
    #endif
    
    // 以ALOGE为例子
    #ifnded ALOGE
    #define ALOGE(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__))
    #endif
    
    #ifndef ALOG
    #define ALOG(priority, tag, ...) 
    LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)
    #endif
    
    #ifndef LOG_PRI
    #define LOG_PRI(priority, tag, ...) 
    android_printLog(priority, tag, __VA_ARGS__)
    #endif
    
    # 回到了我们熟悉的__android_log_print函数
    #define android_printLog(prio, tag, fmt...)
    __android_log_print(prio, tag, fmt)

    Java日志写入接口
    Android系统在应用程序框架中定义了三个Java日志写入接口,它们分别是android.util.Log、android.util.Slog和android.util.EventLog,写入的日志记录类型分别为main、system和events。
    这里主要分析android.util.log的实现。源码如下:

    public final class Log {
    
    /**
    * Priority constant for the println method; use Log.v.
    */
    public static final int VERBOSE = 2;
    
    /**
    * Priority constant for the println method; use Log.d.
    */
    public static final int DEBUG = 3;
    
    /**
    * Priority constant for the println method; use Log.i.
    */
    public static final int INFO = 4;
    
    /**
    * Priority constant for the println method; use Log.w.
    */
    public static final int WARN = 5;
    
    /**
    * Priority constant for the println method; use Log.e.
    */
    public static final int ERROR = 6;
    
    /**
    * Priority constant for the println method.
    */
    public static final int ASSERT = 7;
    
    private Log() {
    }
    
    /**
    * Send a {@link #VERBOSE} log message.
    * @param tag Used to identify the source of a log message. It usually identifies
    * the class or activity where the log call occurs.
    * @param msg The message you would like logged.
    */
    public static int v(String tag, String msg) {
    return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
    }
    
    /**
    * Send a {@link #DEBUG} log message.
    * @param tag Used to identify the source of a log message. It usually identifies
    * the class or activity where the log call occurs.
    * @param msg The message you would like logged.
    */
    public static int d(String tag, String msg) {
    return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
    }
    
    /**
    * Send an {@link #INFO} log message.
    * @param tag Used to identify the source of a log message. It usually identifies
    * the class or activity where the log call occurs.
    * @param msg The message you would like logged.
    */
    public static int i(String tag, String msg) {
    return println_native(LOG_ID_MAIN, INFO, tag, msg);
    }
    
    /**
    * Send a {@link #WARN} log message.
    * @param tag Used to identify the source of a log message. It usually identifies
    * the class or activity where the log call occurs.
    * @param msg The message you would like logged.
    */
    public static int w(String tag, String msg) {
    return println_native(LOG_ID_MAIN, WARN, tag, msg);
    }
    
    /**
    * Send an {@link #ERROR} log message.
    * @param tag Used to identify the source of a log message. It usually identifies
    * the class or activity where the log call occurs.
    * @param msg The message you would like logged.
    */
    public static int e(String tag, String msg) {
    return println_native(LOG_ID_MAIN, ERROR, tag, msg);
    }
    
    /** @hide */ public static final int LOG_ID_MAIN = 0;
    /** @hide */ public static final int LOG_ID_RADIO = 1;
    /** @hide */ public static final int LOG_ID_EVENTS = 2;
    /** @hide */ public static final int LOG_ID_SYSTEM = 3;
    
    /** @hide */ public static native int println_native(int bufID,
    int priority, String tag, String msg);
    }

    可以看到,JAVA应用层logger代码是调用了JNI层的android_util_Log.cpp,源码如下:

    static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,
    jint bufID, jint priority, jstring tagObj, jstring msgObj)
    {
    const char* tag = NULL;
    const char* msg = NULL;
    
    if (msgObj == NULL) {
    jniThrowNullPointerException(env, "println needs a message");
    }
    
    if (bufID < 0 || bufID >= LOG_ID_MAX) {
    jniThrowNullPointerException(env, "bad bufID");
    return -1;
    }
    
    if (tagObj != NULL) {
    tag = env->GetStringUTFChars(tagObj, NULL);
    }
    msg = env->GetStringUTFChars(msgObj, NULL);
    int res = -1;
    // 真正日志写入的函数(liblog.so中的函数)
    res = __android_log_buf_write(bufID, (android_LogPriority), tag, msg);
    return res;
    }

    logcat工具分析
    前面分析的将日志记录写入到Logger日志中的目的就是通过logcat工具将它们读出来,然后给开发人员进行分析。
    Logcat的用法很多,但是这里主要从源码的角度出发,分析Logcat的四个部分:

    基础数据结构。
    初始化过程。
    日志记录的读取过程。
    日志记录的输出过程。
    logcat的源码位于:system/core/logcat.cpp中。

    基础数据结构
    首先是定义在system/core/include/log/logger.h中的logger_entry,定义如下:

    struct logger_entry {
    uint16_t len;
    uint16_t __pad;
    int32_t pid;
    int32_t tid;
    int32_t sec;
    int32_t nsec;
    char msg[0];
    };

    结构体logger_entry用来描述一条日志记录。其中,char msg[0]指针用来记录消息实体内容。

    然后,在看一下queued_entry_t结构体,源码如下:

    struct queued_entry_t {
    union {
    unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1] __attribute__((aligned(4)));
    struct logger_entry entry __attribute__((aligned(4)));
    };
    queued_entry_t* next;
    
    queued_entry_t() {
    next = NULL;
    }
    };

    结构体queued_entry_t用来描述一个日志记录队列。每一种类型的日志记录都对应有一个日志记录队列。

    接下来,再来看一下日志设备的结构体log_device_t。源码如下:

    struct log_device_t {
    char* device;
    bool binary;
    int fd;
    bool printed;
    char label;
    
    queued_entry_t* queue;
    log_device_t* next;
    
    log_device_t(char* d, bool b, char l) {
    device = d;
    binary = b;
    label = l;
    queue = NULL;
    next = NULL;
    printed = false;
    }
    
    void enqueue(queued_entry_t* entry) {
    if (this->queue == NULL) {
    this->queue = entry;
    } else {
    queued_entry_t** e = &this->queue;
    while (*e && cmp(entry, *e) >= 0) }{
    e = &((*e)->next);
    }
    entry->next = *e;
    *e = entry;
    }
    }
    };

    结构体log_device_t用来描述一个日志设备。其中:

    成员变量device保存的是日志设备文件名称。Logger日志驱动程序初始化时,会创建四个设备文件/dev/log/main、/dev/log/system、/dev/log/radio和/dev/log/events分别代表四个日志设备。
    成员变量label用来描述日志设备的标号,其中,日志设备/dev/log/main、/dev/log/system、/dev/log/radio、/dev/log/events对应的标号分别为m、s、r、e。
    成员binary是一个布尔值,表示日志记录的内容是否是二进制格式的。目前只有/dev/log/events记录的是二进制格式。
    成员变量fd是一个文件描述符,它是调用函数open来打开相应的日志设备文件得到的,用来从logger日志驱动程序中读取日志记录。
    成员变量printed是一个布尔值,用来表示一个日志设备是否已经处于输出状态。
    成员变量queue用来保存日志设备中的日志记录。
    成员变量next用来连接下一个日志设备。
    成员函数enqueue用来将一条日志记录添加到内部的日志记录队列中。每次往队列中加入一条日志记录时,都会根据它的写入时间来找到它在队列中的位置,然后将它插入队列中。写入的时间比较是通过cmp函数实现的。

    static int cmp(queued_entry_t* a, queued_entry_t* b)
    {
    int n = a->entry.sec - b->entry.sec;
    if (n != 0) {
    return n;
    }
    
    return a->entry.nsec - b->entry.nsec;
    }

    其实,我觉得cpm函数没什么好解释的,真正有意思的是enqueue函数的实现。这里使用了一个技巧,通过二级指针来减少变量声明(ps:通常我们在做链表插入操作的时候,一般会维护两个指针)。
    二级指针的精髓在于,可以让我们修改指针的值。(ps:想要修改指针的值,就需要修改指针的指针)。

    真正日志打印的时候,需要转换成AndroidLogEntry结构体,相关的结构体定义如下:

    typedef struct AndroidLogEntry_t {
    time_t tv_sec;
    long tv_nsec;
    android_LogPriority priority;
    int msg_type;
    int32_t pid;
    int32_t tid;
    const char *tag;
    size_t messageLen;
    const char *message;
    } AndroidLogEntry;
    
    typedef enum android_LogPriority {
    ANDROID_LOG_UNKNOWN = 0,
    ANDROID_LOG_DEFAULT,
    ANDROID_LOG_VERBOSE,
    ANDROID_LOG_DEBUG,
    ANDROID_LOG_INFO,
    ANDROID_LOG_WARN,
    ANDROID_LOG_ERROR,
    ANDROID_LOG_FATAL,
    ANDROID_LOG_SILENT,
    } android_LogPriority;

    同时,还有一个结构体FilterInfo_t用来描述日志记录输出过滤器。源码如下:

    typedef struct FilterInfo_t {
    char *mTag;
    android_LogPriority mPri;
    struct FilterInfo_t *p_next;
    } FilterInfo;

    成员变量mTag和mPri分别表示要过滤的日志记录的标签和优先级。当一条日志记录的标签等于mTag时,如果它的优先级大于等于mPri,那么它就会被输出,否则就会被忽略。成员变量p_next用来连接下一个日志输出过滤器。

    最后,再介绍AndroidLogFormat_t结构体。

    struct AndroidLogFormat_t {
    android_LogPriority global_pri;
    FilterInfo *filters;
    AndroidLogPrintFormat format;
    };

    从结构体定义上就可以知道,这个结构体是用来保存日志记录的输出格式以及输出过滤器的。

    初始化过程
    Logcat工具的初始化过程是从文件logcat.cpp中的main函数开始的,它会打开日志设备和解析命令行参数。由于函数较长,需要分段解释一下。

    // 数据结构定义
    struct AndroidLogFormat_t {
    android_LogPriority global_pri;
    FilterInfo *filters;
    AndroidLogPrintFormat format;
    };
    typedef struct AndroidLogFormat_t AndroidLogFormat;
    
    
    // 变量声明
    static AndroidLogFormat *g_logformat;
    
    // 函数定义
    AndroidlogFormat *android_log_format_new()
    {
    AndroidLogFormat *p_ret;
    
    p_ret = calloc(1, sizeof(AndroidLogFormat));
    p_ret->global_pri = ANDROID_LOG_VERBOSE;
    p_ret->format = FORMAT_BRIEF;
    
    return p_ret;
    }
    
    // main函数
    int main(int argc, char **argv)
    {
    int err = 0;
    int hasSetLogFormat = 0;
    int clearLog = 0;
    int getLogSize = 0;
    int mode = O_RDONLY;
    const char *forceFilters = NULL;
    log_device_t *devices = NULL;
    log_device_t *dev;
    bool needBinary = false;
    
    g_logformat = android_log_format_new();
    }

    从函数android_log_format_new的实现可以看出,全局变量g_logformat指定了日志的默认输出格式为FOTAMT_BRIEF,指定了日志记录过滤优先级为ANDROID_LOG_VERBOSE.

    回到main函数,我们继续分析main函数对传入参数的解析过程源码如下:

    #define LOG_FILE_DIR "/dev/log/"
    
    for (;;) {
    int ret;
    ret = getopt(argc, argv, "cdt:gsQf:r::n:v:b:B");
    if (ret < 0) break;
    
    switch(ret) {
    case 'd':
    // logcat日志驱动程序中没有日志记录可读时,logcat工具直接退出
    g_nonblock = true;
    break;
    
    case 't':
    // 同d选项,并且指定每次日志输出的条数为g_tail_lines
    g_nonblock = true;
    g_tail_lines = atoi(optarg);
    break;
    
    case 'b': {
    // 通过-b参数指定需要打开的日志文件(main,system,radio,events),并将其设备数据结构添加到链表devices中
    char* buf = (char*) malloc(strlen(LOG_FILE_DIR) + strlen(optarg) + 1);
    strcpy(buf, LOG_FILE_DIR);
    strcat(buf, optarg);
    
    bool binary = strcmp(optarg, "events") == 0;
    if (binary) {
    needBinary = true;
    }
    
    if (devices) {
    dev = devices;
    while (dev->next) {
    dev = dev->next;
    }
    dev->next = new log_device_t(buf, binary, optarg[0]);
    } else {
    devices = new log_device_t(buf, binary, optarg[0]);
    }
    android::g_devCount ++;
    }
    break;
    
    case 'B':
    // 表示以二进制格式输出日志
    android::g_printBinary = 1;
    break;
    
    case 'f':
    // 日志记录输出文件的名称
    android::g_outputFileName = optarg;
    break;
    
    case 'r':
    // 记录每一个日志记录输出文件的最大容量
    if (optarg == NULL) {
    android::g_logRotateSizeKBytes = DEFAULT_LOG_ROTATE_SIZE_KBYTES;
    } else {
    long logRotateSize;
    char *lastDigit;
    
    if (!isdigit(optarg[0])) {
    fprintf(stderr, "Invalid parameter to -r
    ");
    exit(-1);
    }
    android::g_logRotateSizeKBytes = atoi(optarg);
    }
    break;
    
    case 'n':
    if (!isdigit(optarg[0])) {
    // 这里有个Android源码的Bug,源码里写的是-r(太粗心了吧!!)
    fprintf(stderr, "Invalid parameter to -n
    ");
    exit(-1);
    }
    android:g_maxRotatedLogs = atoi(optarg);
    break;
    
    case 'v':
    // 设置日志输出格式
    err = setLogFormat(optarg);
    if (err < 0) {
    fprintf(stderr, "Invalid parameter to -v
    ");
    exit(-1);
    }
    hasSetLogFormat = 1;
    break;
    }

    这段代码主要是解析参数,每一个参数的含义我已经通过注释写到代码里去了。

    解析完命令行参数后,代码继续往后执行:

    if (!devices) {
    devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false, 'm');
    android::g_devCount = 1;
    int accessmode = (mode & O_RDONLY) ? R_OK : 0 | (mode & O_WRONLY) ? W_OK : 0;
    // 如果/dev/log/system文件存在,默认也读取system日志
    if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) {
    devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's');
    android::g_devCount ++;
    }
    }

    这段代码的主要作用是:当用户没有指定-b参数时,默认将main和system的log输出到logcat中。

    接下来,继续分析main函数源码。

    android:setupOutput();
    
    static void setupOutput()
    {
    if (g_outputFileName == NULL) {
    // logcat的默认输出为标准输出
    g_outFD = STDOUT_FILENO;
    } else {
    // 使用-f选项指定了输出
    struct stat statbuf;
    
    g_outFD = openLogFile(g_outputFileName);
    if (g_outFD < 0) {
    perror("Couldn't open output file");
    exit(-1);
    }
    
    fstat(g_outFD, &statbuf);
    g_outByteCount = statbuf.st_size;
    }
    }

    回到main函数,继续向下阅读源码。

    if (hasSetLogFormat == 0) {
    const char* logFormat = getenv("ANDROID_PRINTF_LOG");
    
    if (logFormat != NULL) {
    err = setLogFormat(logFormat);
    if (err < 0) {
    fprintf(stderr, "invalid format in ANDROID_PRINTF_LOG '%s'
    ", logFormat);
    }
    }
    }

    这块代码的主要作用是:当用户没有指定特定的输出格式时,logcat会查一下ANDROID_PRINTF_LOG的值,如果这个值设置的话,就将日志格式改为这个值。

    设置好logcat日志输出格式后,logcat会继续向下执行。

    if (forceFilters) {
    // 不需要管这里
    } else if (argc == optind){
    // 不需要care
    } else {
    // 增加logcat过滤器
    for (int i = optind; i < argc; i ++) {
    err = android_log_addFilterString(g_logformat, argv[i]);
    
    if (err < 0) {
    fprintf(stderr, "Invalid filter expression '%s'
    ", argv[i]);
    exit(-1);
    }
    }
    }
    
    int android_log_addFilterString(AndroidLogFormat *p_format, const char *filterString)
    {
    char *filterStringCopy = strdup(filterString);
    char *p_cur = filterStringCopy;
    char *p_ret;
    int err;
    
    while (NULL != (pret = strsep(&p_cur, " 	,"))) {
    if (p_ret[0] != '') {
    err = android_log_addFilterRule(p_format, p_ret);
    if (err < 0) {
    goto err;
    }
    }
    }
    
    free(filterStringCopy);
    return 0;
    error:
    free(filterStringCopy);
    return -1;
    }
    
    int android_log_addFilterRule(AndroidLogFormat *p_format, const char *filterExpression) {
    size_t i = 0;
    size_t tagNameLength;
    android_LogPriority pri = ANDROID_LOG_DEFAULT;
    
    // 获取tag的长度
    tagNameLength = strcspn(filterExpression, ":");
    if (tagNameLength == 0) {
    goto err;
    }
    
    // 获取tag对应的日志权限pri
    if (filterExpression[tagNameLength] == ':') {
    pri = filterCharToPri(filterExpresion[tagNameLength + 1]);
    if (pri == ANDROID_LOG_UNKNOWN) {
    goto err;
    }
    }
    
    if (0 == strncmp("*", filterExpression, tagNameLength)) {
    // *默认是打印当前tag的所有级别的log
    if (pri == ANDROID_LOG_DEFAULT) {
    pri = ANDROID_LOG_DEBUG;
    }
    p_format->global_pri = pri;
    } else {
    if (pri == ANDROID_LOG_DEFAULT) {
    pri = ANDROID_LOG_VERBOSE;
    }
    
    char *tagName;
    tagName = strdup(filterExpression);
    tagName[tagNameLength] = '';
    
    FilterInfo *p_fi = filterinfo_new(tagName, pri);
    free(tagName);
    
    // 头插法将过滤条件插入
    p_fi->p_next = p_format->filters;
    p_format->filters = p_fi;
    }
    return 0;
    error:
    return -1;
    }

    logcat日志过滤格式为:[:priority]。其中,tag为任意字符串,代表一个日志记录标签。priority是一个字符,表示一个日志记录优先级。增加了过滤后,也是代表日志记录tag-过滤tag时,只有priority大于过滤priority的日志才会被输出。

    日志记录的读取过程
    Logcat工具是从源代码文件logcat.cpp中的函数readLogLines开始读取日志记录的,我们来分段阅读这个函数的实现。

    android::readLogLines(devices);
    1
    函数实现如下:
    
    static void readLogLines(log_device_t* devices)
    {
    log_device_t* dev;
    int max = 0;
    int ret;
    int queued_lines = 0;
    bool sleep = false;
    
    int result;
    fd_set readset;
    
    for (dev = devices; dev; dev = dev->next) {
    if (dev->fd > max) {
    max = dev->fd;
    }
    }
    
    while (1) {
    do {
    timeval timeout = {0, 5000};
    FD_ZERO(&readset);
    for (dev = devices; dev; dev = dev->next) {
    FD_SET(dev->fd, &readset);
    }
    result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout);
    } while (result == -1 && errno == EINTR);
    }
    }

    由于Logcat工具有可能打开了多个日志设备,因此,while循环中使用了select函数来同时监控他们是否有内容可读,即是否有新日志需要读取。调用select函数时,需要设定用来查找这些打开的日志设备中的最大文件描述符,并保存在变量max中。

    当代码跳出select时,是存在两种可能性的。

    当前logcat有新日志可读。
    select选择超时,当前无新日志可读。
    首先分析当前日志设备有新的日志记录可读的情况,如下所示:

    if (result >= 0) {
    for (dev = devices; dev; dev = dev->next) {
    if (FD_ISSET(dev->fd, &readset)) {
    queued_entry_t* entry = new queued_entry_t();
    ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);
    if (ret < 0) {
    exit(EXIT_FAILURE);
    } else if (!ret) {
    exit(EXIT_FAILURE);
    } else if (entry->entry.len != ret - sizeof(struct logger_entry)) {
    exit(EXIT_FAILURE);
    
    
    entry->entry.msg[entry->entry.len] = '';
    dev->enqueue(entry);
    ++queued_lines;
    }
    }
    }

    每当设备有新数据可读时,就取出新数据构造queued_entry_t结构体,并插入到队列entry中,并且queued_lines全局变量+1。

    if (result == 0) {
    sleep = true;
    while (true) {
    chooseFirst(devices. &dev);
    if (dev == NULL) {
    break;
    }
    if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {
    printNextEntry(dev);
    } else {
    skipNextEntry(dev);
    }
    -- queued_lines;
    }
    
    if (g_nonblock) {
    return;
    }
    } else {
    sleep = false;
    while (g_tail_lines == 0 || queued_lines > g_tail_lines) {
    chooseFirst(devices, &dev);
    if (dev == NULL || dev->queue->next == NULL) {
    if (entry_too_match) {
    trigger_log(dev);
    } else {
    break;
    }
    }
    if (g_tail_lines == 0) {
    printnextEntry(dev);
    } else {
    skipNextEntry(dev);
    }
    --queued_lines;
    }
    }

    由于Logcat工具是按照写入时间的先后顺序来输出日志记录的,因此,在输出已经读取的日志记录之前,Logcat工具会首先调用chooseFirst找到包含有最早的未输出日志记录的日志设备,源码实现如下:

    static void chooseFirst(log_device_t* dev, log_device_t** firstdev)
    {
    if (*firstdev = NULL; dev != NULL; dev = dev->next) {
    if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) {
    *firstdev = dev;
    }
    }
    }

    chooseFirst函数只是用来找到最早的日志记录,而日志真正的输出是通过函数printNextEntry实现的。源码实现如下:

    static void printNextEntry(log_device_t* dev)
    {
    maybePrintStart(dev);
    processBuffer(dev, &dev->queue->entry);
    skipNextEntry(dev);
    }

    其中,maybePrintStart是用例判断当前日志设备dev中的日志记录是否是第一次输出,如果是第一次输出,会增加一些特定标志信息。

    static void maybePrintStart(log_device_t* dev)
    {
    if (!dev->printed) {
    dev->printed = true;
    if (g_devCount > 1 && !g_printBinary) {
    char buf[1024];
    snprintf(buf, sizeof(buf), "--------- beginning of %s
    ", dev->device);
    if (write(g_outFD, buf, strlen(buf)) < 0) {
    exit(-1);
    }
    }
    }
    }

    日志输出后,就需要将日志从队列中删除,这是通过调用函数skipNextEntry实现的,源码如下:

    static void skipNextEntry(log_device_t* dev)
    {
    maybePrintStart(dev);
    queued_entry_t* entry = dev->queue;
    dev->queue = entry->next;
    delete entry;
    entry_num --;
    }


    ---------------------
    原文:https://blog.csdn.net/wzy_1988/article/details/47341121

  • 相关阅读:
    mysql
    Linux下的C语言基础-4
    Linux下的C语言基础-3
    Linux下的C语言基础-2
    Linux下的C语言基础-1
    LeetCode:1375. 灯泡开关 III
    Jenkins+robotframework持续集成环境(三)
    Jenkins+robotframework持续集成环境(二)
    Jenkins+robotframework持续集成环境(一)
    robotframework操作使用
  • 原文地址:https://www.cnblogs.com/Ph-one/p/10858579.html
Copyright © 2020-2023  润新知