前两篇文章主要从应用到内核,把日志的写入过程说完了,现在我们来说一下,日志的读取过程。一般对于我们开发者来说,在开发应用过程中,日志对应用的功能实现是没有影响的,主要是在出现问题的时候,我们会去从日志文件中寻找线索,所以读取日志的过程其实就是从前面说的写入的日志文件中,把日志给解析出来,其实就是写日志的一个逆向过程,日志在上层有Logcat工具,这个相信大家都用过,读取日志就是通过Logcat来从日志文件中读取的,一般我们都会用adb logcat命令来输出日志,这个大家应该都用过,所以首先来看下Logcat的初始化,全部代码比较长,先看一下完整的代码,然后我们一点点来解释。
```c
int main(int argc, char **argv)
{
int err;
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(); // 创建一个全局的日志输出格式和输出对象过滤器
if (argc == 2 && 0 == strcmp(argv[1], "--test")) {
logprint_run_tests();
exit(0);
}
if (argc == 2 && 0 == strcmp(argv[1], "--help")) {
android::show_help(argv[0]);
exit(0);
}
for (;;) {
int ret;
ret = getopt(argc, argv, "cdt:gsQf:r::n:v:b:B"); // getop函数返回值表示命令行中的选项,选项对应的值保存在optarg变量中。getopt是命令行解析函数
if (ret < 0) {
break;
}
switch(ret) {
case 's':
// default to all silent
android_log_addFilterRule(g_logformat, "*:s"); // 插入一个过滤器到全局格式过滤器中
break;
case 'c': // 清除日志
clearLog = 1;
mode = O_WRONLY; // 设置成只写模式
break;
case 'd':
g_nonblock = true; // g_nonblock表示如果没有日志可读,logcat退出
break;
case 't':
g_nonblock = true;
g_tail_lines = atoi(optarg); // g_tail_lines表示输出日志的条数,atoi把字符串转为整数,ret为t的情况下参数optarg是数字
break;
case 'g': // 查看日志设备缓冲区大小
getLogSize = 1;
break;
case 'b': { // 这个应该是开始时候要打开那个日志设备,如果没有使用b,默认是main
char* buf = (char*) malloc(strlen(LOG_FILE_DIR) + strlen(optarg) + 1); // 此时optarg应该是类似main,radio,events
strcpy(buf, LOG_FILE_DIR); // 把LOG_FILE_DIR复制到buf中
strcat(buf, optarg); // 把optarg追加到buf后面
bool binary = strcmp(optarg, "events") == 0; // 比较字符串,如果相等等于0
if (binary) {
needBinary = true;
}
if (devices) { // 如果设备不为空
dev = devices; // 把devices赋值给dev
while (dev->next) { // 让dev遍历到最后一个空位置
dev = dev->next;
}
dev->next = new log_device_t(buf, binary, optarg[0]); // 创建该设备挂在设备链表最后
} else {
devices = new log_device_t(buf, binary, optarg[0]); // buf设备命字或者说路径,类似dev/log/main,binary是否是events,这里的optarg是类似main,radio这种,这里取第一个字符作为label
}
android::g_devCount++; // 打开的日志设备个数+1
}
break;
case 'B':
android::g_printBinary = 1; // 表示用二进制来输出日志
break;
case 'f':
// redirect output to a file
android::g_outputFileName = optarg; // 日志输出的文件名,可能会输出多个文件,比如第一个文件是file,后面就是file.1,file.2等以此类推
break;
case 'r': // 结合上面f输出文件,这里optarg表示每个文件最大输出字节数
if (optarg == NULL) { // 如果optarg是空,就要默认的DEFAULT_LOG_ROTATE_SIZE_KBYTES,16字节,
android::g_logRotateSizeKBytes
= DEFAULT_LOG_ROTATE_SIZE_KBYTES;
} else {
long logRotateSize;
char *lastDigit;
if (!isdigit(optarg[0])) { // 是否大于0
fprintf(stderr,"Invalid parameter to -r\n");
android::show_help(argv[0]);
exit(-1);
}
android::g_logRotateSizeKBytes = atoi(optarg); // 每个输出文件的最大字节数是optarg
}
break;
case 'n': // 这里也是要结合上面f,表示最多允许输出到多少个文件
if (!isdigit(optarg[0])) { // 和上面r一样,大于等于0数字
fprintf(stderr,"Invalid parameter to -r\n");
android::show_help(argv[0]);
exit(-1);
}
android::g_maxRotatedLogs = atoi(optarg);
break;
case 'v':
err = setLogFormat (optarg); // 转换成AndroidLogPrintFormat日志输出格式
if (err < 0) { // 格式无效则报错
fprintf(stderr,"Invalid parameter to -v\n");
android::show_help(argv[0]);
exit(-1);
}
hasSetLogFormat = 1; // 表示格式已经设置好了
break;
case 'Q':
/* this is a *hidden* option used to start a version of logcat */
/* in an emulated device only. it basically looks for androidboot.logcat= */
/* on the kernel command line. If something is found, it extracts a log filter */
/* and uses it to run the program. If nothing is found, the program should */
/* quit immediately */
#define KERNEL_OPTION "androidboot.logcat="
#define CONSOLE_OPTION "androidboot.console="
{
int fd;
char* logcat;
char* console;
int force_exit = 1;
static char cmdline[1024];
fd = open("/proc/cmdline", O_RDONLY);
if (fd >= 0) {
int n = read(fd, cmdline, sizeof(cmdline)-1 );
if (n < 0) n = 0;
cmdline[n] = 0;
close(fd);
} else {
cmdline[0] = 0;
}
logcat = strstr( cmdline, KERNEL_OPTION );
console = strstr( cmdline, CONSOLE_OPTION );
if (logcat != NULL) {
char* p = logcat + sizeof(KERNEL_OPTION)-1;;
char* q = strpbrk( p, " \t\n\r" );;
if (q != NULL)
*q = 0;
forceFilters = p;
force_exit = 0;
}
/* if nothing found or invalid filters, exit quietly */
if (force_exit)
exit(0);
/* redirect our output to the emulator console */
if (console) {
char* p = console + sizeof(CONSOLE_OPTION)-1;
char* q = strpbrk( p, " \t\n\r" );
char devname[64];
int len;
if (q != NULL) {
len = q - p;
} else
len = strlen(p);
len = snprintf( devname, sizeof(devname), "/dev/%.*s", len, p );
fprintf(stderr, "logcat using %s (%d)\n", devname, len);
if (len < (int)sizeof(devname)) {
fd = open( devname, O_WRONLY );
if (fd >= 0) {
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
}
break;
default:
fprintf(stderr,"Unrecognized Option\n");
android::show_help(argv[0]);
exit(-1);
break;
}
}
if (!devices) { // 如果需要打开的设备没有设置
devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false, 'm'); // 默认打开main设备,文本形式输出
android::g_devCount = 1; // 打开设备+1
int accessmode =
(mode & O_RDONLY) ? R_OK : 0
| (mode & O_WRONLY) ? W_OK : 0;
// only add this if it's available
if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) { // 判断system设备是否存在
devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's'); // 可以取得的话挂在devices后面
android::g_devCount++;
}
}
if (android::g_logRotateSizeKBytes != 0
&& android::g_outputFileName == NULL // 如果没有输出的文件名,但是有每个文件输出的最大字节数,报错
) {
fprintf(stderr,"-r requires -f as well\n");
android::show_help(argv[0]);
exit(-1);
}
android::setupOutput(); // 设置输出到文件还是标准输出中
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);
}
}
}
if (forceFilters) { // 这个好像是给模拟器用的
err = android_log_addFilterString(g_logformat, forceFilters); // 尝试把这个日志输出过滤器加入到全局格式过滤器中
if (err < 0) {
fprintf (stderr, "Invalid filter expression in -logcat option\n");
exit(0);
}
} else if (argc == optind) {
// Add from environment variable
char *env_tags_orig = getenv("ANDROID_LOG_TAGS"); // 获取环境变量过滤表达式
if (env_tags_orig != NULL) {
err = android_log_addFilterString(g_logformat, env_tags_orig); // 把这个过滤表达式加入全局日志过滤格式器中
if (err < 0) {
fprintf(stderr, "Invalid filter expression in"
" ANDROID_LOG_TAGS\n");
android::show_help(argv[0]);
exit(-1);
}
}
} else {
// Add from commandline
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]);
android::show_help(argv[0]);
exit(-1);
}
}
}
dev = devices;
while (dev) { // 这里开始的循环主要的工作是循环变量所有日志设备,把设备的文件描述符保存在dev->fd中,初始化的时候没有初始化fd
dev->fd = open(dev->device, mode); // 保存fd到设备中
if (dev->fd < 0) {
fprintf(stderr, "Unable to open log device '%s': %s\n",
dev->device, strerror(errno));
exit(EXIT_FAILURE);
}
if (clearLog) { // 这时候mode会是只写模式,可能是清除log缓冲区
int ret;
ret = android::clearLog(dev->fd);
if (ret) {
perror("ioctl");
exit(EXIT_FAILURE);
}
}
if (getLogSize) {
int size, readable;
size = android::getLogSize(dev->fd); // 获取log缓冲区大小
if (size < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
readable = android::getLogReadableSize(dev->fd); // 获取log可读取log的大小
if (readable < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
printf("%s: ring buffer is %dKb (%dKb consumed), "
"max entry is %db, max payload is %db\n", dev->device,
size / 1024, readable / 1024,
(int) LOGGER_ENTRY_MAX_LEN, (int) LOGGER_ENTRY_MAX_PAYLOAD);
}
dev = dev->next;
}
if (getLogSize) {
return 0;
}
if (clearLog) {
return 0;
}
//LOG_EVENT_INT(10, 12345);
//LOG_EVENT_LONG(11, 0x1122334455667788LL);
//LOG_EVENT_STRING(0, "whassup, doc?");
if (needBinary)
android::g_eventTagMap = android_openEventTagMap(EVENT_TAG_MAP_FILE);
android::readLogLines(devices); // 初始化好了所有日志设备和数据,开始读取
return 0;
}
```
这个代码虽然比较长,但是主要的思路就是从日志文件中读取数据,然后根据日志格式来解析数据,最后读取数据输出。
首先我们要创建一个日志格式过滤器,这个的作用是我们会日志的tag和优先级来过滤需要显示的日志,记得adb logcat tag:d 类似这种日志命令吗,他会选取tag并且优先级是d的日志输出,这个过滤器就是把这些日志的标签和优先级保留下来,然后输出的时候,根据这些规则来显示,我们可以看下这个类:
```c
typedef struct FilterInfo_t { // 日志记录过滤器。当一条标签为mTag的日志,如果优先级>=mPri就会被输出,否则会被过滤掉
char *mTag; // 日志的标签
android_LogPriority mPri; // 日志的优先级
struct FilterInfo_t *p_next; // 下一个日志过滤器
} FilterInfo;
struct AndroidLogFormat_t { // 如果日志过滤器中某一个过滤器的过滤优先级被设置为ANDROID_LOG_DEFAULT时,系统会把这个过滤器优先级修改为global_pri
android_LogPriority global_pri; // 日志优先级
FilterInfo *filters; // 日志过滤列表
AndroidLogPrintFormat format; // 日志输出格式
};
```
类AndroidLogFormat_t就保存了所有的过滤列表和日志输出的格式,列表也是一个类FilterInfo_t,其实就是每个过滤规则,FilterInfo_t有个字段p_next,指向下一个过滤规则,所谓的列表就是一个个过滤规则通过p_next字段连起来的链表,format这个字段保存了日志输出的格式,我们继续看代码,遇到用到这些的地方,大家就会知道具体怎么使用了。
如果logcat命令的参数是--test,--help,这些都是调试或者帮助命令的参数,程序调用相应的函数后,这里初始化也就结束了,这些就不多说了,我们继续看和输出命令有关的。
我们敲adb logcat命令的后面都会跟参数,比如-f输出到文件,-c清理日志等等,由于参数可以有多个,所以程序中会通过一个for循环来执行,下面就是开始根据这些参数来做相应的的操作。
for循环开始会调用getopt命令来获取这些参数,这个方法是操作系统根据main函数的两个参数argc,argv来解析的,我们只要记住,返回结果代表参数的类型,参数的值会保存在变量optarg中。参数类型还是比较多的,我们下面主要分析一些比较重要的。
```c
case 'd':
g_nonblock = true; // g_nonblock表示如果没有日志可读,logcat退出
```
这个参数d,表示如果日志中没有数据可以读取了,便结束logcat,这个在后边输出日志的地方可以看到,大家记住这里给变量g_nonblock赋值为true
```c
case 't':
g_nonblock = true;
g_tail_lines = atoi(optarg); // g_tail_lines表示输出日志的条数,atoi把字符串转为整数,ret为t的情况下参数optarg是数字
break;
```
接着是参数t,表示输出最近的几条数据,比如logcat -t 3,表示输出最近的3条数据,前面说了参数的值会保存在optarg变量中,所以这里用函数atoi把参数值转为整数,保存在g_tail_lines中。
参数b表示的是哪个日志设备,代码如下:
```c
case 'b': { // 这个应该是开始时候要打开那个日志设备,如果没有使用b,默认是main
char* buf = (char*) malloc(strlen(LOG_FILE_DIR) + strlen(optarg) + 1); // 此时optarg应该是类似main,radio,events
strcpy(buf, LOG_FILE_DIR); // 把LOG_FILE_DIR复制到buf中
strcat(buf, optarg); // 把optarg追加到buf后面
bool binary = strcmp(optarg, "events") == 0; // 比较字符串,如果相等等于0
if (binary) {
needBinary = true;
}
if (devices) { // 如果设备不为空
dev = devices; // 把devices赋值给dev
while (dev->next) { // 让dev遍历到最后一个空位置
dev = dev->next;
}
dev->next = new log_device_t(buf, binary, optarg[0]); // 创建该设备挂在设备链表最后
} else {
devices = new log_device_t(buf, binary, optarg[0]); // buf设备命字或者说路径,类似dev/log/main,binary是否是events,这里的optarg是类似main,radio这种,这里取第一个字符作为label
}
android::g_devCount++; // 打开的日志设备个数+1
}
```
我们知道日志设备有好几种,main,system,radio,event等,他们的文件路径是/dev/log/xxx,如果没有指定b,默认就是main日志设备。首先介绍创建一个这种类型日志设备的字符串,把他们保存到buf中。之前我们有说过,如果是event日志,他们数据是二进制类型,其他的则是文件,所以接下去会判断日志如果是event,把变量binary设备为true。
接着就要创建具体的设备了,日志设备类是log_device_t:
```c
struct log_device_t { // 每个日志设备都有一个这个类
char* device; // 设备名,比如dev/log/main等
bool binary; // 是否是二进制的内容,events是二进制的,其他都是文本
int fd; // 该设备的文件描述符
bool printed; // 是否已经处于输出状态
char label; // 设备标签,四种设备分别对应 m,s,r,e
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) { // 如果第一条日志就是null,说明还没有日志
this->queue = entry; // 插入到头
} else { // 否则就是有日志,计算插入位置
queued_entry_t** e = &this->queue; // 取出头日志,应该是时间最早的
while (*e && cmp(entry, *e) >= 0) { // 如果头日志非空,并且entrt - *e >=0, 说明entry时间比*e晚,继续循环
e = &((*e)->next); // e切到下一条日志
}
entry->next = *e; // 到这里说明找到entry的时间比*e早了,entry指向*e。注意,此时*e是一个queued_entry_t的地址,entry->next中保存了下一个e是一个queued_entry_t的地址,entry元素所在的地址
*e = entry; // 由于上面*e中的地址已经保存到了entry->next中了,所以现在我们可以修改*e的值了,新的值变成entry的地址
}
}
};
```
日志设备的信息都会保存在这个类中,包括这个设备下的所有日志也会用一个队列的形式保存在这个类中,enqueue方法就是把一条日志插入到这个设备中,具体来说这个队列会按照时间先后顺序,老的在队头,新的在后面的顺序插入,具体上面的注释中也写的比较清晰,可以看下上面代码。另外值得注意的是,这里有个变量log_device_t* next,它是指向另一个日志设备的,这里如果打开多个不同的设备会通过这个字段串在一起,这个我们回到之前的main函数就可以看到。
回到main函数中,在创建log_device_t设备时,devices如果是空就直接创建该设备,如果不空,会往后遍历next,直到空为止,然后新创建设备后赋值非next指针。最后把全局打开设备数android::g_devCount加一。
下面在看B参数:
```c
case 'B':
android::g_printBinary = 1; // 表示用二进制来输出日志
break;
```
这个比较简答,如果是用二进制来输出,那么就把g_printBinary置1。
参数f,这个大家用的比较多了,就是输出到文件:
```c
case 'f':
// 日志输出的文件名,可能会输出多个文件,比如第一个文件是file,后面就是file.1,file.2等以此类推
android::g_outputFileName = optarg;
break;
```
这个是把输出的文件指定文件名。下面我们还会看到还可以指定输出文件的数量,当一个文件放不下时候,会输出到下一个文件,下一个文件的名字就是在这个指定的文件名后面加数字,比如指定文件是file,那么后面的文件名就是file1,file2等等。
下面是指定每个文件最大可以包含多少字节:
```c
case 'r': // 结合上面f输出文件,这里optarg表示每个文件最大输出字节数
if (optarg == NULL) { // 如果optarg是空,就要默认的DEFAULT_LOG_ROTATE_SIZE_KBYTES,16字节,
android::g_logRotateSizeKBytes
= DEFAULT_LOG_ROTATE_SIZE_KBYTES;
} else {
long logRotateSize;
char *lastDigit;
if (!isdigit(optarg[0])) { // 是否大于0
fprintf(stderr,"Invalid parameter to -r\n");
android::show_help(argv[0]);
exit(-1);
}
android::g_logRotateSizeKBytes = atoi(optarg); // 每个输出文件的最大字节数是optarg
}
```
optarg是指定的参数值,如果没有指定每个文件最大字节数,默认会给赋值DEFAULT_LOG_ROTATE_SIZE_KBYTES,这个值定义如下:
```c
#define DEFAULT_LOG_ROTATE_SIZE_KBYTES 16 // 这个单位是K,即16K字节
```
即16K字节,如果定义了的话,就转成数字赋值给g_logRotateSizeKBytes变量。
下面是定义最大可以输出的文件数量,
```c
case 'n': // 这里也是要结合上面f,表示最多允许输出到多少个文件
if (!isdigit(optarg[0])) { // 和上面r一样,大于等于0数字
fprintf(stderr,"Invalid parameter to -r\n");
android::show_help(argv[0]);
exit(-1);
}
android::g_maxRotatedLogs = atoi(optarg);
break;
```
这里会判断是否大于0,这里要特别注意下,如果文件数量为0,表示所有都输出到一个文件,和1是一样的,最后把这个值保存在g_maxRotatedLogs中。
最后说的是v:
```c
case 'v':
err = setLogFormat (optarg); // 转换成AndroidLogPrintFormat日志输出格式
if (err < 0) { // 格式无效则报错
fprintf(stderr,"Invalid parameter to -v\n");
android::show_help(argv[0]);
exit(-1);
}
hasSetLogFormat = 1; // 表示格式已经设置好了
break;
```
-v参数是设置输出日志格式的,通过setLogFormat函数把参数值设置给全局日志过滤和格式器上,看下代码:
```c
static int setLogFormat(const char * formatString) // 获取日志格式
{
static AndroidLogPrintFormat format;
format = android_log_formatFromString(formatString);
if (format == FORMAT_OFF) {
// FORMAT_OFF means invalid string
return -1;
}
android_log_setPrintFormat(g_logformat, format); // 把格式设置到全局日志格式器中
return 0;
}
```
这段代码逻辑还是比较清楚的,首先把参数formatString通过方法android_log_formatFromString转化为一个格式枚举类AndroidLogPrintFormat:
```c
typedef enum { // 日志的输出格式
FORMAT_OFF = 0,
FORMAT_BRIEF,
FORMAT_PROCESS,
FORMAT_TAG,
FORMAT_THREAD,
FORMAT_RAW,
FORMAT_TIME,
FORMAT_THREADTIME,
FORMAT_LONG,
} AndroidLogPrintFormat;
```
这个枚举类里面定义了多种格式,最后在日志输出的时候会根据这里保存的值,来输出不同的是的,这个到后面分析输出的地方就会看到,这里先不详细说。回到前面的方法,获得了这个枚举类方法是:
```c
AndroidLogPrintFormat android_log_formatFromString(const char * formatString) // 根据字符串,获取日志输出格式
{
static AndroidLogPrintFormat format;
if (strcmp(formatString, "brief") == 0) format = FORMAT_BRIEF;
else if (strcmp(formatString, "process") == 0) format = FORMAT_PROCESS;
else if (strcmp(formatString, "tag") == 0) format = FORMAT_TAG;
else if (strcmp(formatString, "thread") == 0) format = FORMAT_THREAD;
else if (strcmp(formatString, "raw") == 0) format = FORMAT_RAW;
else if (strcmp(formatString, "time") == 0) format = FORMAT_TIME;
else if (strcmp(formatString, "threadtime") == 0) format = FORMAT_THREADTIME;
else if (strcmp(formatString, "long") == 0) format = FORMAT_LONG;
else format = FORMAT_OFF;
return format;
}
```
可以看到,这个方法就是做字符串的比较,找到相同的字符串后,返回枚举类中对应的项,如果没找到默认返回FORMAT_OFF。获得了这个枚举值后,如果枚举类值正常,则把他设置到全局的格式过滤类中,具方法是android_log_setPrintFormat:
```c
void android_log_setPrintFormat(AndroidLogFormat *p_format,
AndroidLogPrintFormat format) // 设置日志输出格式
{
p_format->format=format;
}
```
这个方法就是把枚举值设置到开头我们说的全局日志格式过滤器上的p_format字段。
好了,基本上关于参数就说这些,这里参数都是比较常用的,更复杂的可以在想研究的时候再回过来看代码。上面命令行参数解析好了,下面继续进行logcat的初始化工作:
```c
if (!devices) { // 如果需要打开的设备没有设置
devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false, 'm'); // 默认打开main设备,文本形式输出
android::g_devCount = 1; // 打开设备+1
int accessmode =
(mode & O_RDONLY) ? R_OK : 0
| (mode & O_WRONLY) ? W_OK : 0;
// only add this if it's available
if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) { // 判断system设备是否存在
devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's'); // 可以取得的话挂在devices后面
android::g_devCount++;
}
}
```
这段代码开始的devices就是刚才解析命令行参数时候设置的打开的日志设备,如果没有日志设备被打开,那么默认就会创建一个main的日志设备,然后把全局日志设备打开数量设置为1,然后再根据是否设置了读写模式进行设置,最后会判断system日志设备是否存在,如果存在的话,也会创建一个system的日志设备,并且挂在devices后面,最后还是全局打开设备数加一。
接着下面还有一句代码:
```c
android::setupOutput(); // 设置输出到文件还是标准输出中
static void setupOutput()
{
if (g_outputFileName == NULL) { // 如果输出文件名为空
g_outFD = STDOUT_FILENO; // 使用标准输出
} else {
struct stat statbuf;
g_outFD = openLogFile (g_outputFileName); // 打开文件
if (g_outFD < 0) {
perror ("couldn't open output file");
exit(-1);
}
fstat(g_outFD, &statbuf); // fstat是这个系统调用,由文件描述符获得文件的状态
g_outByteCount = statbuf.st_size; // 把文件大小保存在g_outByteCount中
}
}
```
如果logcat命令中没有设备输出的文件名,我们就把输出定义到标准输出中,即STDOUT_FILENO,如果有设定输出文件,那么通过g_outFD = openLogFile (g_outputFileName)获得这个文件描述符,然后fstat(g_outFD, &statbuf)这个方法是获得这个文件的一些信息状态,最后把文件大小复制给g_outByteCount。
后面有些代码是有关于模拟器,以及命令行其他参数的设置,这些比较高级的参数我们先不去管他们,这我们看整个日志流程没太多影响,只要理解也是增加一些日志过滤器用的,只有过滤器的用法,后面在输出日志会讲到。
我们知道linux里面一切皆文件,一个日志设备就是一个文件,下面我们需要初始化每个设备的信息:
```c
dev = devices;
while (dev) { // 这里开始的循环主要的工作是循环变量所有日志设备,把设备的文件描述符保存在dev->fd中,初始化的时候没有初始化fd
dev->fd = open(dev->device, mode); // 保存fd到设备中
if (dev->fd < 0) {
fprintf(stderr, "Unable to open log device '%s': %s\n",
dev->device, strerror(errno));
exit(EXIT_FAILURE);
}
if (clearLog) { // 这时候mode会是只写模式,可能是清除log缓冲区
int ret;
ret = android::clearLog(dev->fd);
if (ret) {
perror("ioctl");
exit(EXIT_FAILURE);
}
}
if (getLogSize) {
int size, readable;
size = android::getLogSize(dev->fd); // 获取log缓冲区大小
if (size < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
readable = android::getLogReadableSize(dev->fd); // 获取log可读取log的大小
if (readable < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
printf("%s: ring buffer is %dKb (%dKb consumed), "
"max entry is %db, max payload is %db\n", dev->device,
size / 1024, readable / 1024,
(int) LOGGER_ENTRY_MAX_LEN, (int) LOGGER_ENTRY_MAX_PAYLOAD);
}
dev = dev->next;
}
```
这里对前面获取到的打开的日志设备做遍历,我们知道在进程中打开的文件都有一个文件描述符,通过文件描述符就可以找到文件的inode节点,inode节点里面保存的文件数据的信息,所以通过文件描述符我们就可以获取每个文件的具体数据,所以这里遍历每个设备就是要获得他们的文件描述符。
这个通过open(dev->device, mode)方法打开文件,并获得文件描述符。接着如果命令是adb logcat -c 这样的-c代表清除日志,如果当clearLog为1时,需要把打开的日志设备清0,所以会调用android::clearLog(dev->fd)方法去清0.
另一个getLogSize代表的是adb logcat -g,是查看日志设备缓冲区的大小,如果如果getLogSize为1是,就会打印出日志当前缓冲区大小以及可以读取的大小。
以上adb logcat -c和adb logcat -g命令执行后,logcat在处理完这2种命令后就退出了,不会继续执行下去了。如果不是这2种命令,那么logcat的初始化就基本完成了,下面就开始要输出日志了。
```c
if (needBinary)
android::g_eventTagMap = android_openEventTagMap(EVENT_TAG_MAP_FILE);
android::readLogLines(devices); // 初始化好了所有日志设备和数据,开始输出
```
输出日志前,首先判断是不是event日志,由于event日志保存的是二进制格式,所以如果要输出的话需要配合system/etc/evet-log-tags文件来输出,前面在讲-b命令参数的时候说过,如果日志设备是event的话会把needBinary置位1,所以通过判断needBinary是true,就会把evet-log-tags文件保存到g_eventTagMap中,后面在解析的时候会用到,我们看下android_openEventTagMap方法:
```c
EventTagMap* android_openEventTagMap(const char* fileName) // 取出event文件,准备解析,然后保存到EventTagMap中
{
EventTagMap* newTagMap;
off_t end;
int fd = -1;
newTagMap = calloc(1, sizeof(EventTagMap));
if (newTagMap == NULL)
return NULL;
fd = open(fileName, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "%s: unable to open map '%s': %s\n",
OUT_TAG, fileName, strerror(errno));
goto fail;
}
end = lseek(fd, 0L, SEEK_END); // 移动指针到文件尾,返回从0到尾的长度,即文件长度
(void) lseek(fd, 0L, SEEK_SET); // 重新把文件指针移动到文件头
if (end < 0) {
fprintf(stderr, "%s: unable to seek map '%s'\n", OUT_TAG, fileName);
goto fail;
}
newTagMap->mapAddr = mmap(NULL, end, PROT_READ | PROT_WRITE, MAP_PRIVATE,
fd, 0); // 把文件映射到内存中,返回内存地址. mmap参数: 第一个null表示内存线性区的首地址,让内核自己选所以null,end需要映射文件的长度,PROT_READ | PROT_WRITE是读访问|写访问,MAP_PRIVATE私有型,不允许回写设备,fd文件描述符,0文件映射开始的偏移位置
if (newTagMap->mapAddr == MAP_FAILED) { // 映射失败
fprintf(stderr, "%s: mmap(%s) failed: %s\n",
OUT_TAG, fileName, strerror(errno));
goto fail;
}
newTagMap->mapLen = end; // /system/etc/event-log-tags文件长度
if (processFile(newTagMap) != 0) // 解析event文件,把里面所有的event日志,提取出第一和第二个信息,即tag数字和描述,放到EventTagMap的数组中
goto fail;
return newTagMap;
fail:
android_closeEventTagMap(newTagMap); // 映射失败,取消映射
if (fd >= 0)
close(fd); // 关闭文件描述符,即打开的文件inode在进程的文件数组中去掉
return NULL;
}
```
首先会开辟一个指向EventTagMap对象的指针空间,即newTagMap = calloc(1, sizeof(EventTagMap)),它指向一个EventTagMap对象:
```c
struct EventTagMap { // 描述events日志的内容格式
/* memory-mapped source file; we get strings from here */
void* mapAddr; // system/etc/event-log-tags文件加载到内存后的地址,这个文件是解析event日志用的
size_t mapLen; // 上面这块内存的大小
/* array of event tags, sorted numerically by tag index */
EventTag* tagArray; // EventTag数组,这个数组中有tag号和tag字符串的对应关系
int numTags; // 上面这个数组的大小,event文件的行数
};
typedef struct EventTag { // event日志标签号和标签字符串对应关系
unsigned int tagIndex; // 标签号
const char* tagStr; // 标签字符串
} EventTag;
```
EventTagMap是用来解析event日志的,其中event日志的格式有个tag的编号,这个编号代表了这种日志有什么行为有关,比如像电池之类的,这里这个类中有EventTag这个字段,就是用来把tag的编号和他所描述的字符串对应起来的,它的大小是由另一个字段numTags来表示的。只有这些tag编号从哪里来,就是mapAddr这个指针所指向的文件,这个文件里保存着tag编号和文字描述的数据,这个方法下面马上就会讲到怎么把数据解析出来然后保存到这里的tagArray中,后面马上就会说到,这里不多说了,让我们回到前面继续下去。
接着我们会打开这个解析文件,fd = open(fileName, O_RDONLY),open方法打开这个文件,返回它的文件描述符,然后我们会执行下面2行代码:
```c
end = lseek(fd, 0L, SEEK_END); // 移动指针到文件尾,返回从0到尾的长度,即文件长度
(void) lseek(fd, 0L, SEEK_SET); // 重新把文件偏移位置定位到头
```
第一行通过lseek函数的SEEK_END参数,就是把文件偏移位置定位到末尾,返回从开头偏移0字节到末尾的长度,即文件的大小。第二句代码重新把偏移位置设置到开头。这样我们现在已经得到了文件的大小了。下面我们把文件内容读取到内存上来:
```c
// 把文件映射到内存中,返回内存地址. mmap参数: 第一个null表示内存线性区的首地址,让内核自己选所以null,end需要映射文件的长度,PROT_READ | PROT_WRITE是读访问|写访问,MAP_PRIVATE私有型,不允许回写设备,fd文件描述符,0文件映射开始的偏移位置
newTagMap->mapAddr = mmap(NULL, end, PROT_READ | PROT_WRITE, MAP_PRIVATE,fd, 0);
```
这里调用的是系统方法mmap,我们知道当我们操作外部文件的时候,都需要在内存开辟一块区域后才可以进行读写,如果是写数据的话可能还需要回写到外部设备,这种方法理解起来简单,但是有个问题,就是在性能上比较欠缺,试想如果有多个进程打开同一个文件,他们可能仅仅只是读取文件的内容,没有写的需要,那么按照常规的read方法也是会复制多份数据到进程自己的空间,这样的话对于空间的占用是非常浪费的,而mmap方法不需要复制多份相同的数据,如果多个进程同时读一个文件,那么在内存上只需要保存一份这个文件的数据,每个进程都会有个指向这份文件的映射关系(可以先简单理解为类似指针这样的东西,实际比较复杂,远非一个指针可以解决),这样不同进程就都可以读取到这个文件,而有不必重复重建多次。
我们看mmap函数的参数,一个就是上面说的那个映射关系的地址,这里为null表示让系统自动分配。第二个参数end表示映射文件的大小,第三个参数表示可以对文件读写访问,第四个是是否允许回写到原始文件中,这里MAP_PRIVATE表示不可以,后面fd就是文件描述符,0是从文件最开始映射。最后的返回过结果就是文件在内存中的地址,这样用户进程就可以进程操作了。
上面简单的介绍了下mmap函数,后面有机会会单独说一下linux里面和内存相关的内容。这里现在就获得了解析event文件的地址,把他保存在mapAddr字段中,它的长度就是end。接下去就开始正式的解析了,解析方法是processFile:
```c
static int processFile(EventTagMap* map) // 解析event文件,取出tag数组的数量,解析每个event,设置到EventTagMap的数组和数组大小变量中
{
EventTag* tagArray = NULL;
/* get a tag count */
map->numTags = countMapLines(map); // 计算出event文件行数,可以理解为有几个event_entry
if (map->numTags < 0)
return -1;
//printf("+++ found %d tags\n", map->numTags);
/* allocate storage for the tag index array */
map->tagArray = calloc(1, sizeof(EventTag) * map->numTags); // 分配所有event_entry空间
if (map->tagArray == NULL)
return -1;
/* parse the file, null-terminating tag strings */
if (parseMapLines(map) != 0) { // 解析event文件,把tag标签值和对应的字符串描述,放入EventTagMap数组中
fprintf(stderr, "%s: file parse failed\n", OUT_TAG);
return -1;
}
/* sort the tags and check for duplicates */
if (sortTags(map) != 0) // 对tag数组按照标签号进行排序
return -1;
return 0;
}
```
这里解析主要就是解析前面首的event的标签和他对应的文字描述,由于event的标签是一个整数,所以输出的话转为文字才能让人看得懂,所以这样先通过countMapLines方法获取event的数量:
```c
static int countMapLines(const EventTagMap* map) // 计算event文件的行数,一行即一个event_entry
{
int numTags, unknown;
const char* cp;
const char* endp;
cp = (const char*) map->mapAddr; // event文件的首地址
endp = cp + map->mapLen; // event文件的位地址
numTags = 0;
unknown = 1;
while (cp < endp) { // 指针从首地址开始,当还没到达尾地址时
if (*cp == '\n') { // 如果遇到换行符
unknown = 1; // 设置unknow为1
} else if (unknown) { // unknow==1,肯定是一个新行,由于一个event是以数字开头的,所以一旦在一个新行第一次遇到数字,比如就是一个新行,所以行数numTags++,同时unknown=0
if (isCharDigit(*cp)) { // 这句话的意思等于是,只有在遇到unknow为1的时候,即遇到一个换行符后第一次遇到数字,才算是一个event_entry
/* looks like a tag to me */
numTags++;
unknown = 0;
} else if (isCharWhitespace(*cp)) { // 跳过空格,制表符等
/* might be leading whitespace before tag num, keep going */
} else { // 跳过注释。有可能注释是单独的以后,所以会遇到'\n',从而把unknow置1,所以要重新设置为1
/* assume comment; second pass can complain in detail */
unknown = 0;
}
} else {
/* we've made up our mind; just scan to end of line */
}
cp++;
}
return numTags;
}
```
说这个方法前,我们大致看一下event日志的格式。这是一个典型的event日志,30014 am_proc_start (User|1|5),(PID|1|5),(UID|1|5),(Process Name|3),(Type|3),(Component|3),可以看到event日志是以数字开头的,后面跟着的文字就是这个数字的描述,在后面就是一系列的该进程相关信息,我们这里称它为一个列表,列表的每个元素代表了一个信息类型,比如像这里的(User|1|5)等等。这个信息列表中的每个元素会是一个二元组或者三元组,用符号|来分隔,第一个元素表示了这个信息的类型,比如这里User就是代表用户id,后面1是类型,在后面的5是单位,我们来看下所有这些数字的解释:
```c
数据类型:1:int,2:long,3:string,4:list
数据单位:1:对象个数,2:字节数,3:毫秒,4:分配个数,5:ID,6百分比
```
我们把上面(User|1|5)连起来解释一下,就是USER是一个int型的整数,表示的是一个id,后面其他的二元组和三元组都是同样的解释方法(注意二元组没有数据单位)。好了,这个说到了日志格式就顺便说下,后面再看读取代码的时候也会用到。
我们回到countMapLines方法继续看代码,这个方法是要计算出这个解析文件中一共有几种不同tag类型的数据。这里代码处理的逻辑如下,一旦遇到换行符,就说明接下去可能是一个新的日志tag,但是到底是不是呢,还要看后面接的是不是一个数字,如果是的话就把numTags变量加1,不是的话重新寻找下一个换行符。处理逻辑其实还是比较简单的,但是这个文本文件会不会有错误导致这种处理逻辑出错呢,既然这样处理了,谷歌的工程师应该会确保吧,我们不管这么多了,这个方法处理完就得到了有多少个tag数量了,我们返回processFile继续看代码。
map->tagArray = calloc(1, sizeof(EventTag) * map->numTags)这行代码根据上面计算出的tag数量,分配存储空间,接着就开始调用parseMapLines方法把前面文件里面每一个tag和对应文字存放到EventTagMap的EventTag数组中:
```c
static int parseMapLines(EventTagMap* map) // 解析event文件,取出每个tag值和内容,放入EventTagMap的tag数组中
{
int tagNum, lineStart, lineNum;
char* cp;
char* endp;
cp = (char*) map->mapAddr; // event文件收地址
endp = cp + map->mapLen; // event文件尾地址
/* insist on EOL at EOF; simplifies parsing and null-termination */
if (*(endp-1) != '\n') { // 如果最后不是结束符,报错
fprintf(stderr, "%s: map file missing EOL on last line\n", OUT_TAG);
return -1;
}
tagNum = 0; // event的数量
lineStart = 1; // 类似于countMapLines方法中unknow,用来控制时候遇到了'\n'
lineNum = 1; // 当前所在行号,从1开始。这个变量不确定什么意思,好像也不是行号,不过这个变量的作用就是打印下,没有实际作用,先不管了
while (cp < endp) { // 遍历每个字符
//printf("{%02x}", *cp); fflush(stdout);
if (*cp == '\n') { // 遇到换行符
lineStart = 1; // unknow
lineNum++; // 当前所在行+1
} else if (lineStart) { // 如果前一个遇到的是换行符
if (*cp == '#') { // 貌似是一行尾标志
/* comment; just scan to end */
lineStart = 0;
} else if (isCharDigit(*cp)) { // 当前是一个数字
/* looks like a tag; scan it out */
if (tagNum >= map->numTags) {
fprintf(stderr,
"%s: more tags than expected (%d)\n", OUT_TAG, tagNum);
return -1;
}
if (scanTagLine(&cp, &map->tagArray[tagNum], lineNum) != 0) // 把当前第tagNum个event日志放入数组中
return -1;
tagNum++; // 说明已经成功处理了一个event了,行数加1
lineNum++; // we eat the '\n' 行号+1
/* leave lineStart==1 */
} else if (isCharWhitespace(*cp)) { // 跳过空格,制表符等
/* looks like leading whitespace; keep scanning */
} else {
fprintf(stderr,
"%s: unexpected chars (0x%02x) in tag number on line %d\n",
OUT_TAG, *cp, lineNum);
return -1;
}
} else {
/* this is a blank or comment line */
}
cp++;
}
if (tagNum != map->numTags) { // event解析后的数量,和之前解析文件时候的不一样,报错
fprintf(stderr, "%s: parsed %d tags, expected %d\n",
OUT_TAG, tagNum, map->numTags);
return -1;
}
return 0;
}
```
这里首先取出前面mmap方法得到的文件地址,然后还是遍历这个文件,处理逻辑基本和之前一样,每处理到确定是一个event日志的时候,调用scanTagLine(&cp, &map->tagArray[tagNum], lineNum)方法把tag元素保存到EventTag数组中:
```c
static int scanTagLine(char** pData, EventTag* tag, int lineNum) // 把pData开始的event日志,解析出tag值和tag描述字符,保存在EventTag元素中
{
char* cp = *pData;
char* startp;
char* endp;
unsigned long val;
startp = cp;
while (isCharDigit(*++cp)) // cp前移到第一个非数字
;
*cp = '\0'; // 一般来过了前面的数字后,是空格,把空格设置为结束符
val = strtoul(startp, &endp, 10); // 获取startp开始的数字并返回,endp指向数字后的一个字符,10表示十进制
assert(endp == cp);
if (endp != cp)
fprintf(stderr, "ARRRRGH\n");
tag->tagIndex = val; // 把数字赋值给标签号
while (*++cp != '\n' && isCharWhitespace(*cp)) // 跳过所有空格,制表符等
;
if (*cp == '\n') {
fprintf(stderr,
"%s: missing tag string on line %d\n", OUT_TAG, lineNum);
return -1;
}
tag->tagStr = cp; // 保存数字后第一个字符的地址
while (isCharValidTag(*++cp)) // 跳过数字,字母,下划线
;
if (*cp == '\n') { // 如果是换行符,设置为结束符
/* null terminate and return */
*cp = '\0';
} else if (isCharWhitespace(*cp)) { // 如果是空格,制表符等,设置为结束符
/* CRLF or trailin spaces; zap this char, then scan for the '\n' */
*cp = '\0';
/* just ignore the rest of the line till \n
TODO: read the tag description that follows the tag name
*/
while (*++cp != '\n') { // 前进到下一个换行符
}
} else {
fprintf(stderr,
"%s: invalid tag chars on line %d\n", OUT_TAG, lineNum);
return -1;
}
*pData = cp; // 更新原始数据位置
//printf("+++ Line %d: got %d '%s'\n", lineNum, tag->tagIndex, tag->tagStr);
return 0;
}
```
该方法首先执行while (isCharDigit(*++cp)),cp前移,跳过最开始的数字,然后调用strtoul方法获取开头的数字,然后赋值tag的编号,tag->tagIndex = val。此时cp指向数字后面第一个非数字的位置,需要再继续前移到下一个字符串处,因为按照前面event介绍的格式,下面的字符就是tag的文字描述,所以跳过换行和空格等符号,现在cp就是指向了一个字符,把这个字符复制给文字tag文字的变量tag->tagStr = cp,然后这些文件的结尾肯定是个空格之类的符号,所以还需要把cp前移到第一个空格或者制表符等符号处,填入结束符'\0',至此一个tag标签的编号和文字描述的对应关系就完成了。最后的代码*pData = cp是把那个解析的文件指向当前位置,然后可以进行下一个tag的搜索。
我们回到方法parseMapLines,该方法中最重要的就是上面说的这个scanTagLine,竟然遍历后所有的tag标签就都被保存到了EventTagMap类中了,最后做校验后返回到上一层方法android_openEventTagMap,这个方法基本也就处理完了,再回到最开始的main函数中,至此所有的准备工作都已经完成了,在main方法的最后,调用readLogLines方法,开始真正的输出工作了。
logcat的初始化main方法确实非常的长,到目前为止,在输出前的准备工作就都完成了,可以看到虽然我们平时就是敲几条命令,但是要转化最终的输出还是进过了非常复杂的过程,好了,今天已经说了很多的,还有一个readLogLines方法,我们留到下一篇文章吧,下篇文章见。
Android日志模块解析三