上篇文章说到日志读取的Logcat工具初始化,在准备工作做完后,最后调用了readLogLines方法,今天就开始来说这个方法。还是先把这个方法完整的代码贴一下,可以有个整体的概念。
```c
static void readLogLines(log_device_t* devices)
{
log_device_t* dev;
int max = 0;
int ret;
int queued_lines = 0;
bool sleep = true;
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 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.
FD_ZERO(&readset); // 对readset内容清0
for (dev=devices; dev; dev = dev->next) {
FD_SET(dev->fd, &readset); // 将第dev->fd位置1,fd是int32位,一个进程打开文件不超过32个?readset第几位置1了,就说明这位index是需要检查的文件描述符
}
result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout); // 返回可以用的日志文件描述符的个数,max+1表示一共有多少个文件需要检查,需检查的可读文件的文件描述符,最后一个是超时。返回值是有几个文件是可读的
} while (result == -1 && errno == EINTR); // 如果result大于0,表示有日志可读,等于0表示没有可读的日志,等于-1同时errno=EINTR表示有信号需要处理,需要重新调用select方法
if (result >= 0) { // 日志有可读设备,或者没有可读设备的时候都会进入这里
for (dev=devices; dev; dev = dev->next) {
if (FD_ISSET(dev->fd, &readset)) { // 如果这位是1,即可读的文件描述符
queued_entry_t* entry = new queued_entry_t();
/* NOTE: driver guarantees we read exactly one full entry */
ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN); // 从文件中读取一个缓冲区的大小到entry->buf中,成功的话返回读取的字节数,等于表示已经到文件尾了,没有数据读取
if (ret < 0) {
if (errno == EINTR) { // 有信号打断,重新执行
delete entry;
goto next;
}
if (errno == EAGAIN) { // 不阻塞方法打开,继续没有成功就跳出循环执行后面的
delete entry;
break;
}
perror("logcat read");
exit(EXIT_FAILURE);
}
else if (!ret) { // 没有数据读取
fprintf(stderr, "read: Unexpected EOF!\n");
exit(EXIT_FAILURE);
}
entry->entry.msg[entry->entry.len] = '\0'; // entry->buf和entry.msg使联合体,在内容最后加结束符
dev->enqueue(entry); // 把日志插入到queued_entry_t中
++queued_lines; // 当前正在等待的日志条数,即还没输出打印的日志条数
}
}
if (result == 0) { // 到这里已经把目前有数据的设备插入到queued_entry_t中了,如果这次循环所有日志设备都没有可读的,也即可能是超时了,那么就开始打印日志了
// we did our short timeout trick and there's nothing new
// print everything we have and wait for more data
sleep = true;
while (true) { // 超时了开始输出,直到输出所有
chooseFirst(devices, &dev); // 选取日志时间最早的日志设备赋值给dev
if (dev == NULL) {
break;
}
// 这里如果g_tail_lines是0,就是没有限制输出条数,那么就一直输出到结束
// 如果queued_lines <= g_tail_lines,即未输出条数<=限制的条数,那么也是一直输出到结束
// 如果queued_lines > g_tail_lines,那么就先删除最早的日志,然后符合条件了,一直输出到结束
if (g_tail_lines == 0 || queued_lines <= g_tail_lines) { // g_tail_lines表示最大可以输出日志的条数。等于0表示没有限制最大条数,或者queued_lines未输出的条数<=限制的最大条数,那么开始打印
printNextEntry(dev);
} else {
skipNextEntry(dev); // 删除一条日志
}
--queued_lines;
}
// the caller requested to just dump the log and exit
if (g_nonblock) { // g_nonblock是非阻塞,break
exit(0);
}
} else { // 打这里是设备有读取数据, result > 0
// print all that aren't the last in their list
sleep = false;
// 这里如果g_tail_lines是0,就是没有限制输出条数,那么就一直输出到结束
// 如果queued_lines > g_tail_lines,那么就先删除最早的日志,然后符合条件了,一直输出到结束
// 如果如果queued_lines <= g_tail_lines了,即未输出条数<=限制的条数。这里和上面result=0就不一样了,上面是一直输出到结束,这里不输出了,回到最上面的while循环,等待继续接受数据一直到queued_lines > g_tail_lines或者result=0的时候,才会输出。
while (g_tail_lines == 0 || queued_lines > g_tail_lines) { // 没有限制最大可读日志条数或者目前未输出的大于了最大输出条数
chooseFirst(devices, &dev); // 获取最早日志时间的设备
if (dev == NULL || dev->queue->next == NULL) { // 如果没有可读设备,或者可读设备上没有日志,break
break;
}
if (g_tail_lines == 0) { // 没有限制最大输出日志条数
printNextEntry(dev); // 打印一条
} else { // 进入到这里肯定是 queued_lines > g_tail_lines,即未输出的条数大于限制输出的条数了,所以需要删除一条最早的
skipNextEntry(dev);
}
--queued_lines; // 未输出日志条数减1,上面2个分支情况,无论是打印了一条还是删除了一条,都是要减1
}
}
}
next:
;
}
}
```
这个方法的整体流程是获取所有可读的设备,然后从设备的缓冲区中取出数据打印出来,其实逻辑还是挺简单的。我们开始看下代码,
```c
for (dev=devices; dev; dev = dev->next) { // 获取所有打开日志设备中文件描述符最大的一个
if (dev->fd > max) {
max = dev->fd;
}
}
```
代码开始是遍历所有的设备获取他们中间文件描述符最大的一个,我们知道文件描述符就是一个进程中打开文件保存在数组中的下标,这里取最大值是因为后面的select函数中要用到。
接下去就开始获取设备中的数据了,这里如果没有遇到让logcat退出的机制,那么logcat会一直在这里循环读取数据,如果读取完毕了,会等待新的数据。所以这里最外面有个while的死循环。进入while循环后,会先执行如下代码:
```c
do {
timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.
FD_ZERO(&readset); // 对readset内容清0
for (dev=devices; dev; dev = dev->next) {
FD_SET(dev->fd, &readset); // 将第dev->fd位置1,fd是int32位,一个进程打开文件不超过32个?readset第几位置1了,就说明这位index是需要检查的文件描述符
}
result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout); // 返回可以用的日志文件描述符的个数,max+1表示一共有多少个文件需要检查,需检查的可读文件的文件描述符,最后一个是超时。返回值是有几个文件是可读的
} while (result == -1 && errno == EINTR); // 如果result大于0,表示有日志可读,等于0表示没有可读的日志,等于-1同时errno=EINTR表示有信号需要处理,需要重新调用select方法
```
这部分代码主要是获取已经准备好的日志设备,调用的方法是select方法,这个方法是通过文件描述符来获得当前是否有已经准备好的读写设备。这个方法第一个参数是指需要检查文件描述符中多少个文件,所以前面max获取的是日志设备在文件描述符中最大一个的小标,这里加1后,就是检查文件描述符前max+1个文件。第二个参数是检查的设备是否已经可以读了,这里readset是一个位图,代表着哪些设备需要被select方法测试是否准备好,请看下面代码:
```c
FD_ZERO(&readset); // 对readset内容清0
for (dev=devices; dev; dev = dev->next) {
// 将第dev->fd位置1,fd是int32位,一个进程打开文件不超过32个?readset第几位置1了,就说明这位index是需要检查的文件描述符
FD_SET(dev->fd, &readset);
}
```
可以看到FD_ZERO宏是把readset所有位都清0,然后遍历当前打开的设备,把文件描述符的数字在readset中对应的位置1,就是告诉select请帮我检查下,这位对应的文件设备是否准备好了,如果准备好了,select方法会把对应的readset位置1,如果没有准备好,会置0,返回的当前有几个设备是准备好了的,如果返回结果大于0,我们就可以检查readset中哪位是1,是1的就是准备好的文件。
这里处理完后,我们就可以检查result的值了,如果小于0,说明没有设备准备好,那么我们重新循环检查。如果大于等于0,说明有可能有设备准备好了,我们开始读取准备好的设备的数据:
```c
for (dev=devices; dev; dev = dev->next) {
if (FD_ISSET(dev->fd, &readset)) { // 如果这位是1,即可读的文件描述符
queued_entry_t* entry = new queued_entry_t();
/* NOTE: driver guarantees we read exactly one full entry */
ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN); // 从文件中读取一个缓冲区的大小到entry->buf中,成功的话返回读取的字节数,等于表示已经到文件尾了,没有数据读取
if (ret < 0) {
if (errno == EINTR) { // 有信号打断,重新执行
delete entry;
goto next;
}
if (errno == EAGAIN) { // 不阻塞方法打开,继续没有成功就跳出循环执行后面的
delete entry;
break;
}
perror("logcat read");
exit(EXIT_FAILURE);
}
else if (!ret) { // 没有数据读取
fprintf(stderr, "read: Unexpected EOF!\n");
exit(EXIT_FAILURE);
}
entry->entry.msg[entry->entry.len] = '\0'; // 联合体,最后添加结束符
dev->enqueue(entry); // 把日志插入到queued_entry_t中
++queued_lines; // 当前正在等待的日志条数,即还没输出打印的日志条数
}
}
```
这里我们看到,也是遍历所有的设备,通过FD_ISSET(dev->fd, &readset)检查,如果这位是置1的,那么我们就创建一个queued_entry_t对象,然后代码read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN)是从该日志设备中的缓冲区取出数据赋值给entry->buf,这里需要注意的是entry->buf是一个联合体中的一个字段,我们在看下这个类:
```c
struct queued_entry_t { //每个日志类型都有一个这个队列(其实就是看成一条日志,所有日志都是链起来的,所以没有一个专门的队列),里面保存了按时间顺序链在一起的一条条日志
union { // 联合体,即可以用作一条日志的entry,又可以看做一条日志的缓冲区
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;
}
};
```
可以看到union联合体中有2个字段buf和entry,给buf赋值了,即同时给entry赋值了,他们共享同一段地址,所以这里虽然是给buf赋值,然后通过entry字段也可以取出数据,这个在后面解析日志的时候可以看到就是通过entry来读取数据的。最后调用dev->enqueue(entry)把这条日志插入日志队列中。
上面说的情况是返回值大于等于0的情况,如果等于0了,说明当前设备没有可读的设备或者超时了,所以我们现在可以先把已经有的数据打印出来,代码如下:
```c
sleep = true;
while (true) { // 超时了开始输出,直到输出所有
chooseFirst(devices, &dev); // 选取日志时间最早的日志设备赋值给dev
if (dev == NULL) {
break;
}
// 这里如果g_tail_lines是0,就是没有限制输出条数,那么就一直输出到结束
// 如果queued_lines <= g_tail_lines,即未输出条数<=限制的条数,那么也是一直输出到结束
// 如果queued_lines > g_tail_lines,那么就先删除最早的日志,然后符合条件了,一直输出到结束
if (g_tail_lines == 0 || queued_lines <= g_tail_lines) { // g_tail_lines表示最大可以输出日志的条数。等于0表示没有限制最大条数,或者queued_lines未输出的条数<=限制的最大条数,那么开始打印
printNextEntry(dev);
} else {
skipNextEntry(dev); // 删除一条日志
}
--queued_lines;
}
// the caller requested to just dump the log and exit
if (g_nonblock) { // g_nonblock是非阻塞,break
exit(0);
}
```
这里开始我们看到会调用chooseFirst方法获取日志时间最大的那个设备:
```c
static void chooseFirst(log_device_t* dev, log_device_t** firstdev) { // 获取时间最大的日志的设备,赋值给firstdev
for (*firstdev = NULL; dev != NULL; dev = dev->next) {
if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) { // 如果这个日志设备的队列dev->queue非空,并且队列第一个日志比firstdev的第一个队列日志时间早的话,就赋值给firstdev
*firstdev = dev; // firstdev取时间早的日志设备
}
}
}
```
这个方法会遍历所有设备,然后比较每个队列的队头元素,我们之前说过,当往日志设备队列插入日志的时候,是按照时间从早到晚排序的,所以这里比较每个队列的头元素就可以知道他们的时间先后顺序了。
回到之前的方法,接下去的代码是:
```c
// 这里如果g_tail_lines是0,就是没有限制输出条数,那么就一直输出到结束
// 如果queued_lines <= g_tail_lines,即未输出条数<=限制的条数,那么也是一直输出到结束
// 如果queued_lines > g_tail_lines,那么就先删除最早的日志,然后符合条件了,一直输出到结束
// g_tail_lines表示最大可以输出日志的条数。等于0表示没有限制最大条数,或者queued_lines未输出的条数<=限制的最大条数,那么开始打印
if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {
printNextEntry(dev);
} else {
skipNextEntry(dev); // 删除一条日志
}
```
g_tail_lines是允许最大输出几条日志,记得adb logcat -t命令吗,-t就是指定最大可以输出的日志条数。如果g_tail_lines是0,表示没有限制,如果queued_lines <= g_tail_lines,表示当前需要输出的日志小于等于最大限制的条数,所以只要符合这些条件都是可以输出的,会调用printNextEntry方法输出日志。否则说明超过最大输出的条数了,会调用skipNextEntry删除最早的一条日志。我们先来看skipNextEntry方法:
```c
static void skipNextEntry(log_device_t* dev) { // 删除队头日志
maybePrintStart(dev);
queued_entry_t* entry = dev->queue;
dev->queue = entry->next;
delete entry;
}
```
该方法比较简单,就是把队头元素改成下一个,然后删除队头元素。方法开始的maybePrintStart这个方法,没有实际的作用,只不过在该设备第一次可能要输出的时候会打印一些提示,可以看下代码:
```c
static void maybePrintStart(log_device_t* dev) { // 如果是第一次输出,打印点东西
if (!dev->printed) { // 是否是第一次输出
dev->printed = true; // 是第一次的话,把变量置为true
if (g_devCount > 1 && !g_printBinary) { // 如果打开日志的设备大于1,同时不是以二进制输出的,那么就打印点文字。
char buf[1024];
snprintf(buf, sizeof(buf), "--------- beginning of %s\n", dev->device);
if (write(g_outFD, buf, strlen(buf)) < 0) {
perror("output error");
exit(-1);
}
}
}
}
```
这里根据dev->printed的值,看是否要打印些log。回到前面,前面说了是能够正常输出日志的话,会调用printNextEntry方法,我在也跟进去看下:
```c
static void printNextEntry(log_device_t* dev) { // 打印日志
maybePrintStart(dev); // 如果该设备是第一次输出,打印点东西
if (g_printBinary) { // 如果输出是二进制的用printBinary打印
printBinary(&dev->queue->entry); // 二进制格式打印输出
} else {
processBuffer(dev, &dev->queue->entry); // 文本格式打印输出
}
skipNextEntry(dev); // 删除队头日志,这样也即刚才打印的这个
}
```
同样,这里开始也是根据是否设备是第一次输出,会打印一些log。接着会根据是否需要二进制输出,由前面分析我们知道,adb logcat -B命令是设置是否二进制输出的,所以如果设置了二进制输出,就会调用printBinary输出,否则调用processBuffer。我们先看下printBinary这个方法:
```c
void printBinary(struct logger_entry *buf) // 打印一条日志logger_entry的二进制
{
size_t size = sizeof(logger_entry) + buf->len; // 日志头+实际内容=总大小
int ret;
do {
ret = write(g_outFD, buf, size); // 把从buf开始的size个字节大小写入g_outFD中,g_outFD是文件或标准输出
} while (ret < 0 && errno == EINTR); // 如果到这里重新执行,表示有信号去处理了,需要重新执行
}
```
这个方法很简单,buf就是日志的内容,size_t size = sizeof(logger_entry) + buf->len 这句用来计算日志的大小,然后调用write(g_outFD, buf, size)输出,其中文件描述符g_outFD表示输出的设备,可以是一个文件,也可以是标准输出终端等等,这个在前面分析setupOutput方法有受过,可以到日志模块的第三篇文章中去看一下。我们在返回去看另一个文本输出方法processBuffer:
```c
static void processBuffer(log_device_t* dev, struct logger_entry *buf) // dev某个日志设备,buf这个日志设备队列中的头日志,也即时间最早的那条日志
{
int bytesWritten = 0;
int err;
AndroidLogEntry entry;
char binaryMsgBuf[1024];
if (dev->binary) {
err = android_log_processBinaryLogBuffer(buf, &entry, g_eventTagMap,
binaryMsgBuf, sizeof(binaryMsgBuf)); // 解析events日志,是二进制的
//printf(">>> pri=%d len=%d msg='%s'\n",
// entry.priority, entry.messageLen, entry.message);
} else {
err = android_log_processLogBuffer(buf, &entry); // 解析main,system,radio日志
}
if (err < 0) {
goto error;
}
if (android_log_shouldPrintLine(g_logformat, entry.tag, entry.priority)) { // 这条日志优先级是否允许输出
if (false && g_devCount > 1) { // 永远进不去?
binaryMsgBuf[0] = dev->label;
binaryMsgBuf[1] = ' ';
bytesWritten = write(g_outFD, binaryMsgBuf, 2);
if (bytesWritten < 0) {
perror("output error");
exit(-1);
}
}
bytesWritten = android_log_printLogLine(g_logformat, g_outFD, &entry); // 输出日志entry到文件描述符g_outFD对应的资源,返回输出了多少字节
if (bytesWritten < 0) { // 输出的字节小于0,报错
perror("output error");
exit(-1);
}
}
g_outByteCount += bytesWritten; // 到目前为止该文件描述符对应的资源输出的字节数
if (g_logRotateSizeKBytes > 0 // 如果有设备每个文件可以输出的最大字节数
&& (g_outByteCount / 1024) >= g_logRotateSizeKBytes // 并且当前文件已经输出的字节数已经大于规定的了,需要输出到一个新文件。注意这里g_logRotateSizeKBytes单位是K,所以g_outByteCount要除以1024
) {
rotateLogs(); // 把现有日志文件重命名,保存到其他文件中,下次输出日志可以在现在这个文件中重新输出了
}
error:
//fprintf (stderr, "Error processing record\n");
return;
}
```
方法开始有AndroidLogEntry entry变量,最后解析的日志会保存在这里。接着判断dev->binary是不是true,只有event设备才会true,所以这里如果是event设备,会通过android_log_processBinaryLogBuffer方法来解析日志,如果不是的话就通过android_log_processLogBuffer来解析,android_log_processLogBuffer这个方法比较简单我们看一下代码:
```c
int android_log_processLogBuffer(struct logger_entry *buf,
AndroidLogEntry *entry) // 解析 main, system, radio日志。这三种日志格式是 priority tag msg
{
size_t tag_len;
entry->tv_sec = buf->sec;
entry->tv_nsec = buf->nsec;
entry->priority = buf->msg[0]; // 赋值优先级
entry->pid = buf->pid;
entry->tid = buf->tid; // 线程组id
entry->tag = buf->msg + 1; // 标签名tag
tag_len = strlen(entry->tag); // 获取标签名大小
entry->messageLen = buf->len - tag_len - 3; // 内容大小 = 总内容大小 - 标签名大小 - 1字节优先级 - 2字节'\0'
entry->message = entry->tag + tag_len + 1; // 内容开始位置等于标签名后面的位置开始
return 0;
}
```
现在处理的都是非event日志,所以日志格式是, priority|tag|msg这样的,所以我们把这些数据取出来赋值给AndroidLogEntry类变量。顺便说一下,还记得前面提到过的读取日志时候的联合体吗,这里的buf就是联合体中的元素。解析非event日志的方法就完成了,很简单。解析event就比较复杂了,我们看下android_log_processBinaryLogBuffer方法:
```c
int android_log_processBinaryLogBuffer(struct logger_entry *buf,
AndroidLogEntry *entry, const EventTagMap* map, char* messageBuf,
int messageBufLen)
{
size_t inCount;
unsigned int tagIndex;
const unsigned char* eventData;
entry->tv_sec = buf->sec;
entry->tv_nsec = buf->nsec;
entry->priority = ANDROID_LOG_INFO; // event是没有优先级字段的,用ANDROID_LOG_INFO替代
entry->pid = buf->pid;
entry->tid = buf->tid;
/*
* Pull the tag out.
*/
eventData = (const unsigned char*) buf->msg; // 获取event日志内容
inCount = buf->len; // 判断下event内容长度,小于4return
if (inCount < 4)
return -1;
tagIndex = get4LE(eventData); // 获取前4个字节内容,表示标签,由于
eventData += 4; // 上面tagIndex已经用过了,所以eventData可以往右去4个字节
inCount -= 4; // 内容也减4个字节
if (map != NULL) {
entry->tag = android_lookupEventTag(map, tagIndex); // 获取tag值对应的文字描述
} else {
entry->tag = NULL; // 没有tag内容
}
/*
* If we don't have a map, or didn't find the tag number in the map,
* stuff a generated tag value into the start of the output buffer and
* shift the buffer pointers down.
*/
if (entry->tag == NULL) { // 如果标签的内容为空
int tagLen;
tagLen = snprintf(messageBuf, messageBufLen, "[%d]", tagIndex); // 将标签本身作为内容复制给tag
entry->tag = messageBuf;
messageBuf += tagLen+1; // messageBuf移动到tag后
messageBufLen -= tagLen+1; // 缓冲区的长度减去tag
}
/*
* Format the event log data into the buffer.
*/
char* outBuf = messageBuf;
size_t outRemaining = messageBufLen-1; /* leave one for nul byte */ // 留一个字节放'\0'
int result;
result = android_log_printBinaryEvent(&eventData, &inCount, &outBuf,
&outRemaining); // 开始解析event剩余内容,eventData当前日志剩余内容开始地址,inCount是剩余内容长度,outBuf是缓冲区开始地址,outRemaining是缓冲区长度
if (result < 0) {
fprintf(stderr, "Binary log entry conversion failed\n");
return -1;
} else if (result == 1) { // 这里表示空间不足
if (outBuf > messageBuf) { // 这里表示空间不足,但是也输出了写
/* leave an indicator */
*(outBuf-1) = '!';
} else { // outBuf <= messageBuf,其实就是等于,不会小于。到这里表示完全没有输出
/* no room to output anything at all */
*outBuf++ = '!';
outRemaining--;
}
/* pretend we ate all the data */
inCount = 0; // 如果是空间不足,那么剩余未处理的字节数置0
}
/* eat the silly terminating '\n' */
if (inCount == 1 && *eventData == '\n') { // 如果剩余未处理字节是1,并且是换行符,算是个正常日志
eventData++; // 日志指针加1
inCount--; // 未处理数量减1
}
if (inCount != 0) { // 非等于0,说明还有未处理的,打印下
fprintf(stderr,
"Warning: leftover binary log data (%d bytes)\n", inCount);
}
/*
* Terminate the buffer. The NUL byte does not count as part of
* entry->messageLen.
*/
*outBuf = '\0'; // 最后加个结束符
entry->messageLen = outBuf - messageBuf; // 这条日志总长度
assert(entry->messageLen == (messageBufLen-1) - outRemaining); // 看看 缓冲区长度 - 缓冲区剩余长度 是不是等于内容长度
entry->message = messageBuf; // 赋值内容
return 0;
}
```
方法比较长,我们一点点看。开始几行代码:
```c
entry->tv_sec = buf->sec;
entry->tv_nsec = buf->nsec;
entry->priority = ANDROID_LOG_INFO; // event是没有优先级字段的,用ANDROID_LOG_INFO替代
entry->pid = buf->pid;
entry->tid = buf->tid;
```
这些值都是写日志时候直接根据当时的信息写入的,和event二进制内容无关,先赋值给entry,注意的是event是没有priority的,所以这里赋值ANDROID_LOG_INFO。
```c
eventData = (const unsigned char*) buf->msg; // 获取event内容
inCount = buf->len; // 判断下event内容长度,小于4return
```
接着获取event内容和长度,还记得event日志的格式吗?形如 tag(整数) | tagString(字符串) | value(二元组或三元组)。先通过get4LE方法获取前4个字节的整数值:
```c
static inline uint32_t get4LE(const uint8_t* src) // 最终排序是 src[3]src[2]src[1]src[0],低位在右边,所以要这样处理
{
return src[0] | (src[1] << 8) | (src[2] << 16) | (src[3] << 24);
}
```
这里传入的是二进制的字节流,这里看来android是按照大端模式来排列。什么是大小端模式呢?简单说就是如果一个数据超过1个字节,要么如果按照大端模式排列,那么高字节的数据在低端(即右端),反之如果是小端模式排列的话就是和我们平时的正常逻辑一样,低字节在低端,比如0x1234是我们平时使用的数字,如果按照大端模式排序,在内存中就会是0x3412,即高端的一个字节12被放置在内存的低地址上,低端的34被放在内存的高地址上,所以结果就回事0x3412。小端模式的话就就和正常的一样。为什么要这样做的,小端模式的话和我们平时思维一样这个都能理解,但是大端模式有什么优势呢,简单的说就是由于高字节被存储在了低位,所以很容易获取这个数值的符号,这样便于判断和计算。目前大部分好像都被设计为大端模式,可能确实对于计算有优势吧,所以大家知道大小端是怎么回事就行了,回到代码中。
这个由于是4个字节,又是按照大端模式来的,所以程序中需要重新排列这个数据,是输出的文本符合正常人的理解,所以通过左移动把低地址数据移动到高地址,返回组合起来返回。
获得了tag的值,下面我们就需要获得这个值对应的文字描述,通过entry->tag = android_lookupEventTag(map, tagIndex) 这个方法:
```c
const char* android_lookupEventTag(const EventTagMap* map, int tag) // 二分搜索map中和tag值一样的内容
{
int hi, lo, mid;
lo = 0;
hi = map->numTags-1;
while (lo <= hi) {
int cmp;
mid = (lo+hi)/2;
cmp = map->tagArray[mid].tagIndex - tag;
if (cmp < 0) {
/* tag is bigger */
lo = mid + 1;
} else if (cmp > 0) {
/* tag is smaller */
hi = mid - 1;
} else {
/* found */
return map->tagArray[mid].tagStr;
}
}
return NULL;
}
```
之前我们分析过我们把tag解析文件的内容保存在EventTagMap类的EventTag*数组中了,而且是按照tag值的大小来排序的,所以这里利用二分搜索法,寻找对应的tag值,如果找到了return map->tagArray[mid].tagStr;返回,否则返回空。
返回空的情况下,会把自己作为文字描述赋值给tag:
```c
if (entry->tag == NULL) { // 如果标签的内容为空
int tagLen;
tagLen = snprintf(messageBuf, messageBufLen, "[%d]", tagIndex); // 将标签本身作为内容复制给tag
entry->tag = messageBuf;
messageBuf += tagLen+1; // messageBuf移动到tag后
messageBufLen -= tagLen+1; // 缓冲区的长度减去tag
}
```
上面把tag和tag文字描述都处理完了,最后开始处理后面的二元组或三元组了,使用的方法是result = android_log_printBinaryEvent(&eventData, &inCount, &outBuf,&outRemaining),这个方法其实就是之前写日志的逆向,方法比较长,但是只要理解的event日志格式,就不难理解:
```c
// 解析event剩余内容,pEventData是解析文件当前这个tag项跳过tagindex和tag后开始地址,pEventDataLen是剩余内容长度,pOutBuf是缓冲区开始地址,pOutBufLen是缓冲区长度
static int android_log_printBinaryEvent(const unsigned char** pEventData,
size_t* pEventDataLen, char** pOutBuf, size_t* pOutBufLen)
{
const unsigned char* eventData = *pEventData; // event内容的开始地址
size_t eventDataLen = *pEventDataLen; // 内容的长度
char* outBuf = *pOutBuf; // 缓冲区开始地址
size_t outBufLen = *pOutBufLen; // 缓冲区长度
unsigned char type;
size_t outCount;
int result = 0;
if (eventDataLen < 1) // 内容长度小于1
return -1;
type = *eventData++; // 这条内容的类型,比如int,long,string. event类型数据格式, [tag msg],其中msg的格式又为[type value | type value | .....]多个这样的对,这里type是开始的数据
eventDataLen--; // 上面取出了一个字节,长度减1
//fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);
switch (type) {
case EVENT_TYPE_INT: // 如果是int型
/* 32-bit signed int */
{
int ival;
if (eventDataLen < 4) // int值不会小于4
return -1;
ival = get4LE(eventData); // 取出前4个字节,表示event类型是int的值
eventData += 4; // 内容指针往前走4字节
eventDataLen -= 4; // 内容长度减4字节
outCount = snprintf(outBuf, outBufLen, "%d", ival); // 将int值保存到缓冲区outBuf种,最大不超过outBufLenget
if (outCount < outBufLen) { // 如果标签的长度小于缓冲区空间
outBuf += outCount; // 缓冲区指针前移
outBufLen -= outCount; // 缓冲区空间减
} else {
/* halt output */
goto no_room; // 超过缓冲区空间,报错
}
}
break;
case EVENT_TYPE_LONG: // long类型
/* 64-bit signed long */
{
long long lval;
if (eventDataLen < 8) // long类型长度不小于8
return -1;
lval = get8LE(eventData); // 获取long值
eventData += 8;
eventDataLen -= 8;
outCount = snprintf(outBuf, outBufLen, "%lld", lval); // 把long值保存在缓冲区outBuf中
if (outCount < outBufLen) {
outBuf += outCount;
outBufLen -= outCount;
} else {
/* halt output */
goto no_room; // 空间不够
}
}
break;
case EVENT_TYPE_STRING: // 字符
/* UTF-8 chars, not NULL-terminated */
{
unsigned int strLen;
if (eventDataLen < 4)
return -1;
//可以参看android_util_EventLog_writeEvent_String方法,这里后面的4字节是字符串长度
strLen = get4LE(eventData);
eventData += 4;
eventDataLen -= 4;
if (eventDataLen < strLen) // 判断下这里剩余字符串长度是否小于给出的长度
return -1;
if (strLen < outBufLen) { // 如果剩余缓冲区大小可以放得下字符串,开始复制
memcpy(outBuf, eventData, strLen); // 把从eventData开始的strLen个字节复制到缓冲区outBuf中
outBuf += strLen; // 缓冲区指针前移
outBufLen -= strLen; // 缓冲区大小减少
} else if (outBufLen > 0) { // 到这里的话,说明缓冲区放不下,那么只能放到缓冲区满
/* copy what we can */
memcpy(outBuf, eventData, outBufLen); // 把从eventData开始的outBufLen个字节复制到缓冲区outBuf中
outBuf += outBufLen;
outBufLen -= outBufLen;
goto no_room; // 返回空间不够
}
eventData += strLen; // 原始数据指针前移
eventDataLen -= strLen; // 原始数据大小减少
break;
}
case EVENT_TYPE_LIST: // 列表类型
/* N items, all different types */
{
unsigned char count;
int i;
if (eventDataLen < 1)
return -1;
count = *eventData++; // 第一个字节是元素个数
eventDataLen--; // 读取数据指针减1
if (outBufLen > 0) { // 缓冲区长度大于0
*outBuf++ = '['; // 缓冲区加一个字符[]
outBufLen--; // 缓冲区大小减1
} else { // 否则返回
goto no_room;
}
for (i = 0; i < count; i++) { // 遍历所有元素,其实就是调用自己,因为数据格式都是一样的,递归调用
result = android_log_printBinaryEvent(&eventData, &eventDataLen,
&outBuf, &outBufLen);
if (result != 0) // 返回非0失败
goto bail;
if (i < count-1) { // 循环不是最后一个的话,需要加一个分隔符
if (outBufLen > 0) { // 缓冲区有空间,每个元素之间加一个分隔符","
*outBuf++ = ',';
outBufLen--;
} else {
goto no_room;
}
}
}
if (outBufLen > 0) { // 如果缓冲区还是空间加入字符]
*outBuf++ = ']';
outBufLen--;
} else {
goto no_room;
}
}
break;
default:
fprintf(stderr, "Unknown binary event type %d\n", type);
return -1;
}
bail:
*pEventData = eventData; // 把使用过的数据重新复回给原数据
*pEventDataLen = eventDataLen; // 更新源数据大小
*pOutBuf = outBuf; // 更新原缓冲区
*pOutBufLen = outBufLen; // 更新源缓冲区大小
return result;
no_room:
result = 1;
goto bail;
}
```
这里还是要说下event日志的格式,前面已经说过event日志的格式是(tag | msg)这样的,而msg的格式又是(type | value)这样的,type表示当前这个值是什么类型的,记得二元组和三元组吗,中间有个数据类型,这里的type就是数据类型,这个是event写日志时候写进去的,之前我们在说写日志的时候说过event的写入,不理解的可以去第一篇日志模块分析那边看一下。event写入的日志分为四种类型,对应这里的type,我们这里读取就是那里写入的逆向,理解了event的写入,相信这里读取也很容易理解。
我们回到android_log_printBinaryEvent方法,首先之前获取了event日志的tag,接下来获取type类型,type = *eventData++这句获得type,然后如果类型是int:
```c
switch (type) {
case EVENT_TYPE_INT: // 如果是int型
/* 32-bit signed int */
{
int ival;
if (eventDataLen < 4) // int值不会小于4
return -1;
ival = get4LE(eventData); // 取出前4个字节,表示event类型是int的值
eventData += 4; // 内容指针往前走4字节
eventDataLen -= 4; // 内容长度减4字节
outCount = snprintf(outBuf, outBufLen, "%d", ival); // 将int值保存到缓冲区outBuf种,最大不超过outBufLenget
if (outCount < outBufLen) { // 如果标签的长度小于缓冲区空间
outBuf += outCount; // 缓冲区指针前移
outBufLen -= outCount; // 缓冲区空间减
} else {
/* halt output */
goto no_room; // 超过缓冲区空间,报错
}
}
break;
```
这里ival = get4LE(eventData);获取了int值,然后outCount = snprintf(outBuf, outBufLen, "%d", ival);把他保存在缓冲区outBuf中,如果超出缓冲区空间则会报错。接下去long类型:
```c
case EVENT_TYPE_LONG: // long类型
/* 64-bit signed long */
{
long long lval;
if (eventDataLen < 8) // long类型长度不小于8
return -1;
lval = get8LE(eventData); // 获取long值
eventData += 8;
eventDataLen -= 8;
outCount = snprintf(outBuf, outBufLen, "%lld", lval); // 把long值保存在缓冲区outBuf中
if (outCount < outBufLen) {
outBuf += outCount;
outBufLen -= outCount;
} else {
/* halt output */
goto no_room; // 空间不够
}
}
break;
```
处理方法也是一样的,唯一区别是long需要8个字节所以通过lval = get8LE(eventData)来获取,这个就不多说了。接下去是string类型:
```c
case EVENT_TYPE_STRING: // 字符
/* UTF-8 chars, not NULL-terminated */
{
unsigned int strLen;
if (eventDataLen < 4)
return -1;
//可以参看android_util_EventLog_writeEvent_String方法,这里后面的4字节是字符串长度
strLen = get4LE(eventData);
eventData += 4;
eventDataLen -= 4;
if (eventDataLen < strLen) // 判断下这里剩余字符串长度是否小于给出的长度
return -1;
if (strLen < outBufLen) { // 如果剩余缓冲区大小可以放得下字符串,开始复制
memcpy(outBuf, eventData, strLen); // 把从eventData开始的strLen个字节复制到缓冲区outBuf中
outBuf += strLen; // 缓冲区指针前移
outBufLen -= strLen; // 缓冲区大小减少
} else if (outBufLen > 0) { // 到这里的话,说明缓冲区放不下,那么只能放到缓冲区满
/* copy what we can */
memcpy(outBuf, eventData, outBufLen); // 把从eventData开始的outBufLen个字节复制到缓冲区outBuf中
outBuf += outBufLen;
outBufLen -= outBufLen;
goto no_room; // 返回空间不够
}
eventData += strLen; // 原始数据指针前移
eventDataLen -= strLen; // 原始数据大小减少
break;
}
```
string的处理稍有些不同,这是由于在写入string类型的时候会多写一个string字符串长度的值,可以对照着写入的那边看。这里通过strLen = get4LE(eventData)获取字符串长度,然后判断缓冲区大小是否足够,足够的话通过memcpy(outBuf, eventData, strLen)来写入字符串。最后我们来看下list:
```c
case EVENT_TYPE_LIST: // 列表类型
/* N items, all different types */
{
unsigned char count;
int i;
if (eventDataLen < 1)
return -1;
count = *eventData++; // 第一个字节是元素个数
eventDataLen--; // 读取数据指针减1
if (outBufLen > 0) { // 缓冲区长度大于0
*outBuf++ = '['; // 缓冲区加一个字符[]
outBufLen--; // 缓冲区大小减1
} else { // 否则返回
goto no_room;
}
for (i = 0; i < count; i++) { // 遍历所有元素,其实就是调用自己,因为数据格式都是一样的,递归调用
result = android_log_printBinaryEvent(&eventData, &eventDataLen,
&outBuf, &outBufLen);
if (result != 0) // 返回非0失败
goto bail;
if (i < count-1) { // 循环不是最后一个的话,需要加一个分隔符
if (outBufLen > 0) { // 缓冲区有空间,每个元素之间加一个分隔符","
*outBuf++ = ',';
outBufLen--;
} else {
goto no_room;
}
}
}
if (outBufLen > 0) { // 如果缓冲区还是空间加入字符
*outBuf++ = ']';
outBufLen--;
} else {
goto no_room;
}
}
break;
```
list的处理其实就是把上面几种合并起来,递归调用。首先通过 count = *eventData++获取list中有多少元素,然后for循环遍历递归android_log_printBinaryEvent方法,这个相信大家也都明白,最后把list中的元素都处理完成就结束了。
回到android_log_processBinaryLogBuffer方法,把日志都写入缓冲区后,判断下缓冲区是否溢出等异常情况,最后我们会看到:
```c
*outBuf = '\0'; // 最后加个结束符
entry->messageLen = outBuf - messageBuf; // 这条日志总长度
assert(entry->messageLen == (messageBufLen-1) - outRemaining); // 看看 缓冲区长度 - 缓冲区剩余长度 是不是等于内容长度
entry->message = messageBuf; // 赋值内容
```
这里我们看到在结尾添加结束符,然后把日志内容赋值给entry->message就算完成了。
我们回到前面的processBuffer方法,这里我们已经把日志的内容都获取了,接下去在输出前还需要过滤,因为可能我们使用adb logcat tag: priority这种形式的命令,所以需要根据优先级把不需要的过滤掉,android_log_shouldPrintLine(g_logformat, entry.tag, entry.priority)这个函数就是过滤的函数:
```c
int android_log_shouldPrintLine ( // 判断现在这条日志优先级是否>=过滤器里面的优先级
AndroidLogFormat *p_format, const char *tag, android_LogPriority pri)
{
return pri >= filterPriForTag(p_format, tag); // 如果大于等于,就说明运行输出,返回true
}
```
在android_log_shouldPrintLine这个方法中,可以看到最后返回时候判断当前这个日志的优先级是否大于等于filterPriForTag,如果是的话返回true后面就能正常输出,否则就不会输出。我们看下filterPriForTag这个方法:
```c
static android_LogPriority filterPriForTag(
AndroidLogFormat *p_format, const char *tag) // 在全局过滤器里面寻找是否有这个tag标签的过滤器,如果有的话,返回他对应的优先级
{
FilterInfo *p_curFilter;
for (p_curFilter = p_format->filters
; p_curFilter != NULL
; p_curFilter = p_curFilter->p_next // 遍历所有过滤器
) {
if (0 == strcmp(tag, p_curFilter->mTag)) { // 如果找到一个标签是tag的过滤器
if (p_curFilter->mPri == ANDROID_LOG_DEFAULT) { // 如果设置的是ANDROID_LOG_DEFAULT,就用global里的
return p_format->global_pri;
} else {
return p_curFilter->mPri; // 否则就返回这个过滤器的优先级
}
}
}
return p_format->global_pri;
}
```
这个方法会遍历全局过滤器,然后在过滤器里面找是否有这个tag,如果有的话会返回这个tag的优先级,否则返回默认设置的。这样过滤器也完成了,如果没有被过滤掉的话,就会调用android_log_printLogLine(g_logformat, g_outFD, &entry)来输出:
```c
int android_log_printLogLine( // 输出日志entry到文件描述符fd上
AndroidLogFormat *p_format,
int fd,
const AndroidLogEntry *entry)
{
int ret;
char defaultBuffer[512]; // 处理最终拼接字符串的缓冲区
char *outBuffer = NULL;
size_t totalLen;
outBuffer = android_log_formatLogLine(p_format, defaultBuffer,
sizeof(defaultBuffer), entry, &totalLen); // 根据p_format的输出格式,来输出最终日志,返回给outBuffer
if (!outBuffer)
return -1;
do {
ret = write(fd, outBuffer, totalLen); // 把返回的outBuffer字符串,长度为totalLen,输出到fd中
} while (ret < 0 && errno == EINTR);
if (ret < 0) {
fprintf(stderr, "+++ LOG: write failed (errno=%d)\n", errno);
ret = 0;
goto done;
}
if (((size_t)ret) < totalLen) {
fprintf(stderr, "+++ LOG: write partial (%d of %d)\n", ret,
(int)totalLen);
goto done;
}
done:
if (outBuffer != defaultBuffer) {
free(outBuffer);
}
return ret;
}
```
这个方法里面主要是格式化输出,会调用android_log_formatLogLine这个方法:
```c
char *android_log_formatLogLine (
AndroidLogFormat *p_format,
char *defaultBuffer,
size_t defaultBufferSize,
const AndroidLogEntry *entry,
size_t *p_outLength)
{
#if defined(HAVE_LOCALTIME_R)
struct tm tmBuf;
#endif
struct tm* ptm;
char timeBuf[32];
char headerBuf[128];
char prefixBuf[128], suffixBuf[128];
char priChar;
int prefixSuffixIsHeaderFooter = 0;
char * ret = NULL;
priChar = filterPriToChar(entry->priority); // 获取优先级字符,比如V,I等等
/*
* Get the current date/time in pretty form
*
* It's often useful when examining a log with "less" to jump to
* a specific point in the file by searching for the date/time stamp.
* For this reason it's very annoying to have regexp meta characters
* in the time stamp. Don't use forward slashes, parenthesis,
* brackets, asterisks, or other special chars here.
*/
#if defined(HAVE_LOCALTIME_R)
ptm = localtime_r(&(entry->tv_sec), &tmBuf);
#else
ptm = localtime(&(entry->tv_sec));
#endif
//strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", ptm);
strftime(timeBuf, sizeof(timeBuf), "%m-%d %H:%M:%S", ptm);
/*
* Construct a buffer containing the log header and log message.
*/
size_t prefixLen, suffixLen; // 开始拼接log内容,比如输出android.util.Log.i("LogTag","Log Content.")
switch (p_format->format) {
case FORMAT_TAG:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%c/%-8s: ", priChar, entry->tag); // 拼接前缀优先级和tag -> i/LogTag:
strcpy(suffixBuf, "\n"); suffixLen = 1; // 后缀是换行
break;
case FORMAT_PROCESS:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%c(%5d) ", priChar, entry->pid); // i(LogTag)
suffixLen = snprintf(suffixBuf, sizeof(suffixBuf), // LogTag\n
" (%s)\n", entry->tag);
break;
case FORMAT_THREAD:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%c(%5d:%p) ", priChar, entry->pid, (void*)entry->tid);
strcpy(suffixBuf, "\n");
suffixLen = 1;
break;
case FORMAT_RAW:
prefixBuf[0] = 0;
prefixLen = 0;
strcpy(suffixBuf, "\n");
suffixLen = 1;
break;
case FORMAT_TIME:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%s.%03ld %c/%-8s(%5d): ", timeBuf, entry->tv_nsec / 1000000,
priChar, entry->tag, entry->pid);
strcpy(suffixBuf, "\n");
suffixLen = 1;
break;
case FORMAT_THREADTIME:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%s.%03ld %5d %5d %c %-8s: ", timeBuf, entry->tv_nsec / 1000000,
(int)entry->pid, (int)entry->tid, priChar, entry->tag);
strcpy(suffixBuf, "\n");
suffixLen = 1;
break;
case FORMAT_LONG:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"[ %s.%03ld %5d:%p %c/%-8s ]\n",
timeBuf, entry->tv_nsec / 1000000, entry->pid,
(void*)entry->tid, priChar, entry->tag);
strcpy(suffixBuf, "\n\n");
suffixLen = 2;
prefixSuffixIsHeaderFooter = 1;
break;
case FORMAT_BRIEF:
default:
prefixLen = snprintf(prefixBuf, sizeof(prefixBuf),
"%c/%-8s(%5d): ", priChar, entry->tag, entry->pid);
strcpy(suffixBuf, "\n");
suffixLen = 1;
break;
}
/* snprintf has a weird return value. It returns what would have been
* written given a large enough buffer. In the case that the prefix is
* longer then our buffer(128), it messes up the calculations below
* possibly causing heap corruption. To avoid this we double check and
* set the length at the maximum (size minus null byte)
*/
if(prefixLen >= sizeof(prefixBuf)) // 前后缀如果超过了可容纳空间,减少
prefixLen = sizeof(prefixBuf) - 1;
if(suffixLen >= sizeof(suffixBuf))
suffixLen = sizeof(suffixBuf) - 1;
/* the following code is tragically unreadable */
size_t numLines;
size_t i;
char *p;
size_t bufferSize;
const char *pm;
if (prefixSuffixIsHeaderFooter) {
// we're just wrapping message with a header/footer
numLines = 1;
} else {
pm = entry->message;
numLines = 0;
// The line-end finding here must match the line-end finding
// in for ( ... numLines...) loop below
while (pm < (entry->message + entry->messageLen)) {
if (*pm++ == '\n') numLines++;
}
// plus one line for anything not newline-terminated at the end
if (pm > entry->message && *(pm-1) != '\n') numLines++;
}
// this is an upper bound--newlines in message may be counted
// extraneously
bufferSize = (numLines * (prefixLen + suffixLen)) + entry->messageLen + 1;
if (defaultBufferSize >= bufferSize) {
ret = defaultBuffer;
} else {
ret = (char *)malloc(bufferSize);
if (ret == NULL) {
return ret;
}
}
ret[0] = '\0'; /* to start strcat off */
p = ret;
pm = entry->message;
if (prefixSuffixIsHeaderFooter) {
strcat(p, prefixBuf); // 复制前缀
p += prefixLen;
strncat(p, entry->message, entry->messageLen); // 复制内容
p += entry->messageLen;
strcat(p, suffixBuf); // 复制后缀
p += suffixLen;
} else {
while(pm < (entry->message + entry->messageLen)) {
const char *lineStart;
size_t lineLen;
lineStart = pm;
// Find the next end-of-line in message
while (pm < (entry->message + entry->messageLen)
&& *pm != '\n') pm++;
lineLen = pm - lineStart;
strcat(p, prefixBuf);
p += prefixLen;
strncat(p, lineStart, lineLen);
p += lineLen;
strcat(p, suffixBuf);
p += suffixLen;
if (*pm == '\n') pm++;
}
}
if (p_outLength != NULL) {
*p_outLength = p - ret;
}
return ret;
}
```
这个方法看上去非常长,但是没必要细讲,因为主要是通过adb logcat -v参数可以指定不同的输出格式,具体格式对应的类是:
```c
typedef enum { // 日志的输出格式
FORMAT_OFF = 0,
FORMAT_BRIEF,
FORMAT_PROCESS,
FORMAT_TAG,
FORMAT_THREAD,
FORMAT_RAW,
FORMAT_TIME,
FORMAT_THREADTIME,
FORMAT_LONG,
} AndroidLogPrintFormat;
```
每种格式都有不同的显示方法,这个可以参考下官方文档,这里也就不细说了,最后这个方法可以看到这么几句:
```c
strcat(p, prefixBuf);
strncat(p, lineStart, lineLen);
strcat(p, suffixBuf);
```
这个利用strncat函数把格式化都得字符串拼接起来,看到这个信息大家对这个方法作用也都能理解了。回到android_log_printLogLine方法,格式化好数据后,通过ret = write(fd, outBuffer, totalLen)就输出到对应的设备了。在往上回到processBuffer函数,最后还有一句代码,rotateLogs(),具体实现是:
```c
static void rotateLogs() // 把当前文件后复制到后面,当前文件就又是一个可用的
{
int err;
// Can't rotate logs if we're not outputting to a file
if (g_outputFileName == NULL) { // 如果没有指定输出文件名,return
return;
}
close(g_outFD); // 关闭当前的文件描述符
// 这个逻辑是每次最终使用的还是原始的g_outputFileName名字,这个循环是每次从后往前,即第一次先把倒数第二个文件重命令为倒数一个文件,如果倒数那个文件已经存在了则会被删除,这样就相当于把所有文件往后移一个,最后剩下的第0个,名字还是g_outputFileName这个,所以输出日志还是往这个文件输出
for (int i = g_maxRotatedLogs ; i > 0 ; i--) {
char *file0, *file1;
asprintf(&file1, "%s.%d", g_outputFileName, i); // 给file1字符串赋值,相当于后面的一个文件
if (i - 1 == 0) { // file0是前面的一个文件
asprintf(&file0, "%s", g_outputFileName);
} else {
asprintf(&file0, "%s.%d", g_outputFileName, i - 1);
}
err = rename (file0, file1); // 把前面的文件改名成后面的,如果后面的有这个文件名了,会删除
if (err < 0 && errno != ENOENT) {
perror("while rotating log files");
}
free(file1);
free(file0);
}
g_outFD = openLogFile (g_outputFileName); // 经过上面for循环,只有第0个文件可以使用了,其余都是之前的文件挪过去的。这个打开这个文件获得文件描述符
if (g_outFD < 0) {
perror ("couldn't open output file");
exit(-1);
}
g_outByteCount = 0; // 重新定位了一个新文件,把当前文件已经输出字节大小设置为0
}
```
记得之前说命令行参数的时候,可以指定输出文件的个数吗,这个方法就是对一个文件如果写满写,会切换到下一个文件,如果全部文件写满了会循环使用,这个方法就是在文件写满后,切换文件用的。每次循环把前一个文件复制到后一个,这样写入的文件永远是第一个,后面的文件按时间循序排列。
好了至此为止,完整的一个输出流程就结束了,虽然感觉整个日志模块的逻辑还是挺清晰的,但是其中各种细节还是非常烧脑的,比如日志格式的解析等等,相信深入理解后,应该都能理解谷歌的设计思想了。
Android日志模块解析四