1. 程式人生 > >Android Logger日誌系統

Android Logger日誌系統

chart 入隊 size exceptio 函數定義 詳細 poi exception pointer

文件夾

  • 文件夾
  • 前言
  • 執行時庫層日誌庫liblog
    • 源代碼分析
  • CC日誌寫入接口
  • Java日誌寫入接口
  • logcat工具分析
    • 基礎數據結構
    • 初始化過程
    • 日誌記錄的讀取過程


前言

該篇文章是我的讀書和實踐筆記。參考的是《Android系統源代碼情景分析》。


執行時庫層日誌庫——liblog

Android系統在執行時庫層提供了一個用來和Logger日誌驅動程序進行交互的日誌庫liblog。通過日誌庫liblog提供的接口。應用程序就能夠方便地往Logger日誌驅動程序中寫入日誌記錄。

位於執行時庫層的C/C++日誌寫入接口和位於應用程序框架層的Java日誌寫入接口都是通過liblog庫提供的日誌寫入接口來往Logger日誌驅動程序中寫入日誌記錄的。


源代碼分析

日誌庫liblog提供的日誌記錄寫入接口實如今logd_write.c文件裏,它的源代碼位置為:/system/core/liblog/logd_write.c。

依據寫入的日誌記錄的類型不同,這些函數能夠劃分為三個類別,當中:

  1. 函數__android_log_assert、__android_log_vprint和__android_log_print用來寫入類型為main的日誌記錄。

  2. 函數__android_log_btwrite和__android_log_bwrite用來寫入類型為events的日誌記錄。
  3. 函數__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++宏來封裝日誌寫入接口。之所以這樣做,是為了方便開發同學進行日誌的開關控制,比如不在公布版本號中打開日誌。

三組宏定義分別為:

  1. ALOGV,ALOGD,ALOGI。ALOGW和ALOGE。

    用來記錄類型為main的日誌。

  2. SLOGV,SLOGD,SLOGI,SLOGW和SLOGE,用來寫入類型為system的日誌。
  3. 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的四個部分:

  1. 基礎數據結構。

  2. 初始化過程。
  3. 日誌記錄的讀取過程。
  4. 日誌記錄的輸出過程。

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用來描寫敘述一個日誌設備。

當中:

  1. 成員變量device保存的是日誌設備文件名。Logger日誌驅動程序初始化時。會創建四個設備文件/dev/log/main、/dev/log/system、/dev/log/radio和/dev/log/events分別代表四個日誌設備。

  2. 成員變量label用來描寫敘述日誌設備的標號,當中,日誌設備/dev/log/main、/dev/log/system、/dev/log/radio、/dev/log/events相應的標號分別為m、s、r、e。
  3. 成員binary是一個布爾值,表示日誌記錄的內容是否是二進制格式的。

    眼下僅僅有/dev/log/events記錄的是二進制格式。

  4. 成員變量fd是一個文件描寫敘述符,它是調用函數open來打開相應的日誌設備文件得到的,用來從logger日誌驅動程序中讀取日誌記錄。
  5. 成員變量printed是一個布爾值。用來表示一個日誌設備是否已經處於輸出狀態。
  6. 成員變量queue用來保存日誌設備中的日誌記錄。
  7. 成員變量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\n");
                    exit(-1);
                }
                android::g_logRotateSizeKBytes = atoi(optarg);
            }
        break;

        case ‘n‘:
            if (!isdigit(optarg[0])) {
                // 這裏有個Android源代碼的Bug,源代碼裏寫的是-r(太粗心了吧!

fprintf(stderr, "Invalid parameter to -n\n"); exit(-1); } android:g_maxRotatedLogs = atoi(optarg); break; case ‘v‘: // 設置日誌輸出格式 err = setLogFormat(optarg); if (err < 0) { fprintf(stderr, "Invalid parameter to -v\n"); 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‘\n", 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‘\n", 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, " \t,"))) {
        if (p_ret[0] != ‘\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] = ‘\0‘;

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

函數實現例如以下:

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時,是存在兩種可能性的。

  1. 當前logcat有新日誌可讀。

  2. 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] = ‘\0‘;
            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\n", 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 --;
}

Android Logger日誌系統