- 浏览: 21495367 次
- 性别:
- 来自: 杭州
最新评论
-
ZY199266:
配置文件还需要额外的配置ma
Android 客户端通过内置API(HttpClient) 访问 服务器(用Spring MVC 架构) 返回的json数据全过程 -
ZY199266:
我的一访问为什么是 /mavenwebdemo/WEB-I ...
Android 客户端通过内置API(HttpClient) 访问 服务器(用Spring MVC 架构) 返回的json数据全过程 -
lvgaga:
我又一个问题就是 如果像你的这种形式写。配置文件还需要额外的 ...
Android 客户端通过内置API(HttpClient) 访问 服务器(用Spring MVC 架构) 返回的json数据全过程 -
lvgaga:
我的一访问为什么是 /mavenwebdemo/WEB-I ...
Android 客户端通过内置API(HttpClient) 访问 服务器(用Spring MVC 架构) 返回的json数据全过程 -
y1210251848:
你的那个错误应该是项目所使用的目标框架不支持吧
log4net配置(web中使用log4net,把web.config放在单独的文件中)
Android日志系统Logcat源代码简要分析
在前面两篇文章Android日志系统驱动程序Logger源代码分析和Android应用程序框架层和系统运行库层日志系统源代码中,介绍了Android内核空间层、系统运行库层和应用程序框架层日志系统相关的源代码,其中,后一篇文章着重介绍了日志的写入操作。为了描述完整性,这篇文章着重介绍日志的读取操作,这就是我们在开发Android应用程序时,经常要用到日志查看工具Logcat了。
Logcat工具内置在Android系统中,可以在主机上通过adb logcat命令来查看模拟机上日志信息。Logcat工具的用法很丰富,因此,源代码也比较多,本文并不打算完整地介绍整个Logcat工具的源代码,主要是介绍Logcat读取日志的主线,即从打开日志设备文件到读取日志设备文件的日志记录到输出日志记录的主要过程,希望能起到一个抛砖引玉的作用。
Logcat工具源代码位于system/core/logcat目录下,只有一个源代码文件logcat.cpp,编译后生成的可执行文件位于out/target/product/generic/system/bin目录下,在模拟机中,可以在/system/bin目录下看到logcat工具。下面我们就分段来阅读logcat.cpp源代码文件。
一. Logcat工具的相关数据结构。
这些数据结构是用来保存从日志设备文件读出来的日志记录:
- structqueued_entry_t{
- union{
- unsignedcharbuf[LOGGER_ENTRY_MAX_LEN+1]__attribute__((aligned(4)));
- structlogger_entryentry__attribute__((aligned(4)));
- };
- queued_entry_t*next;
- queued_entry_t(){
- next=NULL;
- }
- };
- structlog_device_t{
- char*device;
- boolbinary;
- intfd;
- boolprinted;
- charlabel;
- queued_entry_t*queue;
- log_device_t*next;
- log_device_t(char*d,boolb,charl){
- device=d;
- binary=b;
- label=l;
- queue=NULL;
- next=NULL;
- printed=false;
- }
- voidenqueue(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;
- }
- }
- };
- structlogger_entry{
- __u16len;/*lengthofthepayload*/
- __u16__pad;/*nomatterwhat,weget2bytesofpadding*/
- __s32pid;/*generatingprocess'spid*/
- __s32tid;/*generatingprocess'stid*/
- __s32sec;/*secondssinceEpoch*/
- __s32nsec;/*nanoseconds*/
- charmsg[0];/*theentry'spayload*/
- };
- #defineLOGGER_ENTRY_MAX_LEN(4*1024)
每个日志设备上下文通过其next成员指针连接起来,每个设备文件上下文的日志记录也是通过next指针连接起来。日志记录队例是按时间戳从小到大排列的,这个log_device_t::enqueue函数可以看出,当要插入一条日志记录的时候,先队列头开始查找,直到找到一个时间戳比当前要插入的日志记录的时间戳大的日志记录的位置,然后插入当前日志记录。比较函数cmp的定义如下:
- staticintcmp(queued_entry_t*a,queued_entry_t*b){
- intn=a->entry.sec-b->entry.sec;
- if(n!=0){
- returnn;
- }
- returna->entry.nsec-b->entry.nsec;
- }
二. 打开日志设备文件。
Logcat工具的入口函数main,打开日志设备文件和一些初始化的工作也是在这里进行。main函数的内容也比较多,前面的逻辑都是解析命令行参数。这里假设我们使用logcat工具时,不带任何参数。这不会影响我们分析logcat读取日志的主线,有兴趣的读取可以自行分析解析命令行参数的逻辑。
分析完命令行参数以后,就开始要创建日志设备文件上下文结构体struct log_device_t了:
- if(!devices){
- devices=newlog_device_t(strdup("/dev/"LOGGER_LOG_MAIN),false,'m');
- android::g_devCount=1;
- intaccessmode=
- (mode&O_RDONLY)?R_OK:0
- |(mode&O_WRONLY)?W_OK:0;
- //onlyaddthisifit'savailable
- if(0==access("/dev/"LOGGER_LOG_SYSTEM,accessmode)){
- devices->next=newlog_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM),false,'s');
- android::g_devCount++;
- }
- }
由于我们假设使用logcat时,不带任何命令行参数,这里的devices变量为NULL,因此,就会默认创建/dev/log/main设备上下文结构体,如果存在/dev/log/system设备文件,也会一并创建。宏LOGGER_LOG_MAIN和LOGGER_LOG_SYSTEM也是定义在system/core/include/cutils/logger.h文件中:
- #defineLOGGER_LOG_MAIN"log/main"
- #defineLOGGER_LOG_SYSTEM"log/system"
往下看,调用setupOutput()函数来初始化输出文件:
- android::setupOutput();
- staticvoidsetupOutput()
- {
- if(g_outputFileName==NULL){
- g_outFD=STDOUT_FILENO;
- }else{
- structstatstatbuf;
- g_outFD=openLogFile(g_outputFileName);
- if(g_outFD<0){
- perror("couldn'topenoutputfile");
- exit(-1);
- }
- fstat(g_outFD,&statbuf);
- g_outByteCount=statbuf.st_size;
- }
- }
如果我们在执行logcat命令时,指定了-f <filename>选项,日志内容就输出到filename文件中,否则,就输出到标准输出控制台去了。
再接下来,就是打开日志设备文件了:
- dev=devices;
- while(dev){
- dev->fd=open(dev->device,mode);
- if(dev->fd<0){
- fprintf(stderr,"Unabletoopenlogdevice'%s':%s\n",
- dev->device,strerror(errno));
- exit(EXIT_FAILURE);
- }
- if(clearLog){
- intret;
- ret=android::clearLog(dev->fd);
- if(ret){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- }
- if(getLogSize){
- intsize,readable;
- size=android::getLogSize(dev->fd);
- if(size<0){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- readable=android::getLogReadableSize(dev->fd);
- if(readable<0){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- printf("%s:ringbufferis%dKb(%dKbconsumed),"
- "maxentryis%db,maxpayloadis%db\n",dev->device,
- size/1024,readable/1024,
- (int)LOGGER_ENTRY_MAX_LEN,(int)LOGGER_ENTRY_MAX_PAYLOAD);
- }
- dev=dev->next;
- }
- staticintclearLog(intlogfd)
- {
- returnioctl(logfd,LOGGER_FLUSH_LOG);
- }
如果执行logcat命令的目的是获取日志内存缓冲区的大小,即getLogSize为true,通过调用android::getLogSize函数实现:
- /*returnsthetotalsizeofthelog'sringbuffer*/
- staticintgetLogSize(intlogfd)
- {
- returnioctl(logfd,LOGGER_GET_LOG_BUF_SIZE);
- }
接着验证日志缓冲区可读内容的大小,即调用android::getLogReadableSize函数:
- /*returnsthereadablesizeofthelog'sringbuffer(thatis,amountofthelogconsumed)*/
- staticintgetLogReadableSize(intlogfd)
- {
- returnioctl(logfd,LOGGER_GET_LOG_LEN);
- }
接下去的printf语句,就是输出日志缓冲区的大小以及可读日志的大小到控制台去了。
继续看下看代码,如果执行logcat命令的目的是清空日志或者获取日志的大小信息,则现在就完成使命了,可以退出程序了:
- if(getLogSize){
- return0;
- }
- if(clearLog){
- return0;
- }
- android::readLogLines(devices);
三. 读取日志设备文件。
读取日志设备文件内容的函数是readLogLines函数:
- staticvoidreadLogLines(log_device_t*devices)
- {
- log_device_t*dev;
- intmax=0;
- intret;
- intqueued_lines=0;
- boolsleep=true;
- intresult;
- fd_setreadset;
- for(dev=devices;dev;dev=dev->next){
- if(dev->fd>max){
- max=dev->fd;
- }
- }
- while(1){
- do{
- timevaltimeout={0,5000/*5ms*/};//Ifweoversleepit'sok,i.e.ignoreEINTR.
- 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);
- if(result>=0){
- for(dev=devices;dev;dev=dev->next){
- if(FD_ISSET(dev->fd,&readset)){
- queued_entry_t*entry=newqueued_entry_t();
- /*NOTE:driverguaranteeswereadexactlyonefullentry*/
- ret=read(dev->fd,entry->buf,LOGGER_ENTRY_MAX_LEN);
- if(ret<0){
- if(errno==EINTR){
- deleteentry;
- gotonext;
- }
- if(errno==EAGAIN){
- deleteentry;
- break;
- }
- perror("logcatread");
- exit(EXIT_FAILURE);
- }
- elseif(!ret){
- fprintf(stderr,"read:UnexpectedEOF!\n");
- exit(EXIT_FAILURE);
- }
- entry->entry.msg[entry->entry.len]='\0';
- dev->enqueue(entry);
- ++queued_lines;
- }
- }
- if(result==0){
- //wedidourshorttimeouttrickandthere'snothingnew
- //printeverythingwehaveandwaitformoredata
- 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;
- }
- //thecallerrequestedtojustdumpthelogandexit
- if(g_nonblock){
- exit(0);
- }
- }else{
- //printallthataren'tthelastintheirlist
- sleep=false;
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- }
- }
- next:
- ;
- }
- }
- do{
- timevaltimeout={0,5000/*5ms*/};//Ifweoversleepit'sok,i.e.ignoreEINTR.
- 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);
- for(dev=devices;dev;dev=dev->next){
- if(FD_ISSET(dev->fd,&readset)){
- queued_entry_t*entry=newqueued_entry_t();
- /*NOTE:driverguaranteeswereadexactlyonefullentry*/
- ret=read(dev->fd,entry->buf,LOGGER_ENTRY_MAX_LEN);
- if(ret<0){
- if(errno==EINTR){
- deleteentry;
- gotonext;
- }
- if(errno==EAGAIN){
- deleteentry;
- break;
- }
- perror("logcatread");
- exit(EXIT_FAILURE);
- }
- elseif(!ret){
- fprintf(stderr,"read:UnexpectedEOF!\n");
- exit(EXIT_FAILURE);
- }
- entry->entry.msg[entry->entry.len]='\0';
- dev->enqueue(entry);
- ++queued_lines;
- }
- }
继续进一步处理日志:
- if(result==0){
- //wedidourshorttimeouttrickandthere'snothingnew
- //printeverythingwehaveandwaitformoredata
- 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;
- }
- //thecallerrequestedtojustdumpthelogandexit
- if(g_nonblock){
- exit(0);
- }
- }else{
- //printallthataren'tthelastintheirlist
- sleep=false;
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- }
- staticvoidchooseFirst(log_device_t*dev,log_device_t**firstdev){
- for(*firstdev=NULL;dev!=NULL;dev=dev->next){
- if(dev->queue!=NULL&&(*firstdev==NULL||cmp(dev->queue,(*firstdev)->queue)<0)){
- *firstdev=dev;
- }
- }
- }
- if(g_tail_lines==0||queued_lines<=g_tail_lines){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
如果result > 0,表明有新的日志可读,这时候的处理方式与result == 0的情况不同,因为这时候还有新的日志可读,所以就不能先急着处理之前已经读出来的日志。这里,分两种情况考虑,如果能设置了只显示最新的g_tail_lines条记录,并且当前已经读出来的日志记录条数已经超过g_tail_lines,就要丢弃,剩下的先不处理,等到下次再来处理;如果没有设备显示最新的g_tail_lines条记录,即g_tail_lines == 0,这种情况就和result == 0的情况处理方式一样,先处理所有已经读出的日志记录,再进入下一次循环。希望读者可以好好体会这段代码:
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- staticvoidskipNextEntry(log_device_t*dev){
- maybePrintStart(dev);
- queued_entry_t*entry=dev->queue;
- dev->queue=entry->next;
- deleteentry;
- }
printNextEntry函数处理日志输出,下一节中继续分析。
四.输出日志设备文件的内容。
从前面的分析中看出,最终日志设备文件内容的输出是通过printNextEntry函数进行的:
- staticvoidprintNextEntry(log_device_t*dev){
- maybePrintStart(dev);
- if(g_printBinary){
- printBinary(&dev->queue->entry);
- }else{
- processBuffer(dev,&dev->queue->entry);
- }
- skipNextEntry(dev);
- }
- voidprintBinary(structlogger_entry*buf)
- {
- size_tsize=sizeof(logger_entry)+buf->len;
- intret;
- do{
- ret=write(g_outFD,buf,size);
- }while(ret<0&&errno==EINTR);
- }
- staticvoidprocessBuffer(log_device_t*dev,structlogger_entry*buf)
- {
- intbytesWritten=0;
- interr;
- AndroidLogEntryentry;
- charbinaryMsgBuf[1024];
- if(dev->binary){
- err=android_log_processBinaryLogBuffer(buf,&entry,g_eventTagMap,
- binaryMsgBuf,sizeof(binaryMsgBuf));
- //printf(">>>pri=%dlen=%dmsg='%s'\n",
- //entry.priority,entry.messageLen,entry.message);
- }else{
- err=android_log_processLogBuffer(buf,&entry);
- }
- if(err<0){
- gotoerror;
- }
- 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("outputerror");
- exit(-1);
- }
- }
- bytesWritten=android_log_printLogLine(g_logformat,g_outFD,&entry);
- if(bytesWritten<0){
- perror("outputerror");
- exit(-1);
- }
- }
- g_outByteCount+=bytesWritten;
- if(g_logRotateSizeKBytes>0
- &&(g_outByteCount/1024)>=g_logRotateSizeKBytes
- ){
- rotateLogs();
- }
- error:
- //fprintf(stderr,"Errorprocessingrecord\n");
- return;
- }
当dev->binary为true,日志记录项是二进制形式,不同于我们在Android日志系统驱动程序Logger源代码分析一文中提到的常规格式:
struct logger_entry | priority | tag | msg
这里我们不关注这种情况,有兴趣的读者可以自已分析,android_log_processBinaryLogBuffer函数定义在system/core/liblog/logprint.c文件中,它的作用是将一条二进制形式的日志记录转换为ASCII形式,并保存在entry参数中,它的原型为:
- /**
- *ConvertabinarylogentrytoASCIIform.
- *
- *ForconveniencewemimictheprocessLogBufferAPI.Thereisno
- *pre-definedoutputlengthforthebinarydata,sincewe'refreetoformat
- *ithoweverwechoose,whichmeanswecan'treallyuseafixed-sizebuffer
- *here.
- */
- intandroid_log_processBinaryLogBuffer(structlogger_entry*buf,
- AndroidLogEntry*entry,constEventTagMap*map,char*messageBuf,
- intmessageBufLen);
- typedefstructAndroidLogEntry_t{
- time_ttv_sec;
- longtv_nsec;
- android_LogPrioritypriority;
- pid_tpid;
- pthread_ttid;
- constchar*tag;
- size_tmessageLen;
- constchar*message;
- }AndroidLogEntry;
- /*
- *Androidlogpriorityvalues,inascendingpriorityorder.
- */
- typedefenumandroid_LogPriority{
- ANDROID_LOG_UNKNOWN=0,
- ANDROID_LOG_DEFAULT,/*onlyforSetMinPriority()*/
- ANDROID_LOG_VERBOSE,
- ANDROID_LOG_DEBUG,
- ANDROID_LOG_INFO,
- ANDROID_LOG_WARN,
- ANDROID_LOG_ERROR,
- ANDROID_LOG_FATAL,
- ANDROID_LOG_SILENT,/*onlyforSetMinPriority();mustbelast*/
- }android_LogPriority;
- /**
- *Splitsawire-formatbufferintoanAndroidLogEntry
- *entryallocatedbycaller.Pointerswillpointdirectlyintobuf
- *
- *Returns0onsuccessand-1oninvalidwireformat(entrywillbe
- *inunspecifiedstate)
- */
- intandroid_log_processLogBuffer(structlogger_entry*buf,
- AndroidLogEntry*entry)
- {
- size_ttag_len;
- entry->tv_sec=buf->sec;
- entry->tv_nsec=buf->nsec;
- entry->priority=buf->msg[0];
- entry->pid=buf->pid;
- entry->tid=buf->tid;
- entry->tag=buf->msg+1;
- tag_len=strlen(entry->tag);
- entry->messageLen=buf->len-tag_len-3;
- entry->message=entry->tag+tag_len+1;
- return0;
- }
调用完android_log_processLogBuffer函数后,日志记录的具体信息就保存在本地变量entry中了,接着调用android_log_shouldPrintLine函数来判断这条日志记录是否应该输出。
在分析android_log_shouldPrintLine函数之前,我们先了解数据结构AndroidLogFormat,这个结构体定义在system/core/liblog/logprint.c文件中:
- structAndroidLogFormat_t{
- android_LogPriorityglobal_pri;
- FilterInfo*filters;
- AndroidLogPrintFormatformat;
- };
- typedefstructFilterInfo_t{
- char*mTag;
- android_LogPrioritymPri;
- structFilterInfo_t*p_next;
- }FilterInfo;
- staticAndroidLogFormat*g_logformat;
- g_logformat=android_log_format_new();
回到android_log_shouldPrintLine函数中,它定义在system/core/liblog/logprint.c文件中:
- /**
- *returns1ifthisloglineshouldbeprintedbasedonitspriority
- *andtag,and0ifitshouldnot
- */
- intandroid_log_shouldPrintLine(
- AndroidLogFormat*p_format,constchar*tag,android_LogPrioritypri)
- {
- returnpri>=filterPriForTag(p_format,tag);
- }
- staticandroid_LogPriorityfilterPriForTag(
- AndroidLogFormat*p_format,constchar*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)){
- if(p_curFilter->mPri==ANDROID_LOG_DEFAULT){
- returnp_format->global_pri;
- }else{
- returnp_curFilter->mPri;
- }
- }
- }
- returnp_format->global_pri;
- }
回到processBuffer函数中,如果执行完android_log_shouldPrintLine函数后,表明当前日志记录应当输出,则调用android_log_printLogLine函数来输出日志记录到文件fd中, 这个函数也是定义在system/core/liblog/logprint.c文件中:
- intandroid_log_printLogLine(
- AndroidLogFormat*p_format,
- intfd,
- constAndroidLogEntry*entry)
- {
- intret;
- chardefaultBuffer[512];
- char*outBuffer=NULL;
- size_ttotalLen;
- outBuffer=android_log_formatLogLine(p_format,defaultBuffer,
- sizeof(defaultBuffer),entry,&totalLen);
- if(!outBuffer)
- return-1;
- do{
- ret=write(fd,outBuffer,totalLen);
- }while(ret<0&&errno==EINTR);
- if(ret<0){
- fprintf(stderr,"+++LOG:writefailed(errno=%d)\n",errno);
- ret=0;
- gotodone;
- }
- if(((size_t)ret)<totalLen){
- fprintf(stderr,"+++LOG:writepartial(%dof%d)\n",ret,
- (int)totalLen);
- gotodone;
- }
- done:
- if(outBuffer!=defaultBuffer){
- free(outBuffer);
- }
- returnret;
- }
processBuffer函数的最后,还有一个rotateLogs的操作:
- staticvoidrotateLogs()
- {
- interr;
- //Can'trotatelogsifwe'renotoutputtingtoafile
- if(g_outputFileName==NULL){
- return;
- }
- close(g_outFD);
- for(inti=g_maxRotatedLogs;i>0;i--){
- char*file0,*file1;
- asprintf(&file1,"%s.%d",g_outputFileName,i);
- if(i-1==0){
- asprintf(&file0,"%s",g_outputFileName);
- }else{
- asprintf(&file0,"%s.%d",g_outputFileName,i-1);
- }
- err=rename(file0,file1);
- if(err<0&&errno!=ENOENT){
- perror("whilerotatinglogfiles");
- }
- free(file1);
- free(file0);
- }
- g_outFD=openLogFile(g_outputFileName);
- if(g_outFD<0){
- perror("couldn'topenoutputfile");
- exit(-1);
- }
- g_outByteCount=0;
- }
logfile,logfile.1,logfile.2,logfile.3
当当前输入到logfile文件的日志记录大小g_outByteCount大于等于g_logRotateSizeKBytes时,就要将logfile.2的内容移至logfile.3中,同时将logfile.1的内容移至logfile.2中,同时logfle的内容移至logfile.1中,再重新打开logfile文件进入后续输入。这样做的作用是不至于使得日志文件变得越来越来大,以至于占用过多的磁盘空间,而是只在磁盘上保存一定量的最新的日志记录。这样,旧的日志记录就会可能被新的日志记录所覆盖。
至此,关于Android日志系统源代码,我们就完整地分析完了,其中包括位于内核空间的驱动程序Logger源代码分析,还有位于应用程序框架层和系统运行库层的日志写入操作接口源代码分析和用于日志读取的工具Logcat源代码分析,希望能够帮助读者对Android的日志系统有一个清晰的认识。
在前面两篇文章Android日志系统驱动程序Logger源代码分析和Android应用程序框架层和系统运行库层日志系统源代码中,介绍了Android内核空间层、系统运行库层和应用程序框架层日志系统相关的源代码,其中,后一篇文章着重介绍了日志的写入操作。为了描述完整性,这篇文章着重介绍日志的读取操作,这就是我们在开发Android应用程序时,经常要用到日志查看工具Logcat了。
Logcat工具内置在Android系统中,可以在主机上通过adb logcat命令来查看模拟机上日志信息。Logcat工具的用法很丰富,因此,源代码也比较多,本文并不打算完整地介绍整个Logcat工具的源代码,主要是介绍Logcat读取日志的主线,即从打开日志设备文件到读取日志设备文件的日志记录到输出日志记录的主要过程,希望能起到一个抛砖引玉的作用。
Logcat工具源代码位于system/core/logcat目录下,只有一个源代码文件logcat.cpp,编译后生成的可执行文件位于out/target/product/generic/system/bin目录下,在模拟机中,可以在/system/bin目录下看到logcat工具。下面我们就分段来阅读logcat.cpp源代码文件。
一. Logcat工具的相关数据结构。
这些数据结构是用来保存从日志设备文件读出来的日志记录:
- structqueued_entry_t{
- union{
- unsignedcharbuf[LOGGER_ENTRY_MAX_LEN+1]__attribute__((aligned(4)));
- structlogger_entryentry__attribute__((aligned(4)));
- };
- queued_entry_t*next;
- queued_entry_t(){
- next=NULL;
- }
- };
- structlog_device_t{
- char*device;
- boolbinary;
- intfd;
- boolprinted;
- charlabel;
- queued_entry_t*queue;
- log_device_t*next;
- log_device_t(char*d,boolb,charl){
- device=d;
- binary=b;
- label=l;
- queue=NULL;
- next=NULL;
- printed=false;
- }
- voidenqueue(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;
- }
- }
- };
- structlogger_entry{
- __u16len;/*lengthofthepayload*/
- __u16__pad;/*nomatterwhat,weget2bytesofpadding*/
- __s32pid;/*generatingprocess'spid*/
- __s32tid;/*generatingprocess'stid*/
- __s32sec;/*secondssinceEpoch*/
- __s32nsec;/*nanoseconds*/
- charmsg[0];/*theentry'spayload*/
- };
- #defineLOGGER_ENTRY_MAX_LEN(4*1024)
每个日志设备上下文通过其next成员指针连接起来,每个设备文件上下文的日志记录也是通过next指针连接起来。日志记录队例是按时间戳从小到大排列的,这个log_device_t::enqueue函数可以看出,当要插入一条日志记录的时候,先队列头开始查找,直到找到一个时间戳比当前要插入的日志记录的时间戳大的日志记录的位置,然后插入当前日志记录。比较函数cmp的定义如下:
- staticintcmp(queued_entry_t*a,queued_entry_t*b){
- intn=a->entry.sec-b->entry.sec;
- if(n!=0){
- returnn;
- }
- returna->entry.nsec-b->entry.nsec;
- }
二. 打开日志设备文件。
Logcat工具的入口函数main,打开日志设备文件和一些初始化的工作也是在这里进行。main函数的内容也比较多,前面的逻辑都是解析命令行参数。这里假设我们使用logcat工具时,不带任何参数。这不会影响我们分析logcat读取日志的主线,有兴趣的读取可以自行分析解析命令行参数的逻辑。
分析完命令行参数以后,就开始要创建日志设备文件上下文结构体struct log_device_t了:
- if(!devices){
- devices=newlog_device_t(strdup("/dev/"LOGGER_LOG_MAIN),false,'m');
- android::g_devCount=1;
- intaccessmode=
- (mode&O_RDONLY)?R_OK:0
- |(mode&O_WRONLY)?W_OK:0;
- //onlyaddthisifit'savailable
- if(0==access("/dev/"LOGGER_LOG_SYSTEM,accessmode)){
- devices->next=newlog_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM),false,'s');
- android::g_devCount++;
- }
- }
由于我们假设使用logcat时,不带任何命令行参数,这里的devices变量为NULL,因此,就会默认创建/dev/log/main设备上下文结构体,如果存在/dev/log/system设备文件,也会一并创建。宏LOGGER_LOG_MAIN和LOGGER_LOG_SYSTEM也是定义在system/core/include/cutils/logger.h文件中:
- #defineLOGGER_LOG_MAIN"log/main"
- #defineLOGGER_LOG_SYSTEM"log/system"
往下看,调用setupOutput()函数来初始化输出文件:
- android::setupOutput();
- staticvoidsetupOutput()
- {
- if(g_outputFileName==NULL){
- g_outFD=STDOUT_FILENO;
- }else{
- structstatstatbuf;
- g_outFD=openLogFile(g_outputFileName);
- if(g_outFD<0){
- perror("couldn'topenoutputfile");
- exit(-1);
- }
- fstat(g_outFD,&statbuf);
- g_outByteCount=statbuf.st_size;
- }
- }
如果我们在执行logcat命令时,指定了-f <filename>选项,日志内容就输出到filename文件中,否则,就输出到标准输出控制台去了。
再接下来,就是打开日志设备文件了:
- dev=devices;
- while(dev){
- dev->fd=open(dev->device,mode);
- if(dev->fd<0){
- fprintf(stderr,"Unabletoopenlogdevice'%s':%s\n",
- dev->device,strerror(errno));
- exit(EXIT_FAILURE);
- }
- if(clearLog){
- intret;
- ret=android::clearLog(dev->fd);
- if(ret){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- }
- if(getLogSize){
- intsize,readable;
- size=android::getLogSize(dev->fd);
- if(size<0){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- readable=android::getLogReadableSize(dev->fd);
- if(readable<0){
- perror("ioctl");
- exit(EXIT_FAILURE);
- }
- printf("%s:ringbufferis%dKb(%dKbconsumed),"
- "maxentryis%db,maxpayloadis%db\n",dev->device,
- size/1024,readable/1024,
- (int)LOGGER_ENTRY_MAX_LEN,(int)LOGGER_ENTRY_MAX_PAYLOAD);
- }
- dev=dev->next;
- }
- staticintclearLog(intlogfd)
- {
- returnioctl(logfd,LOGGER_FLUSH_LOG);
- }
如果执行logcat命令的目的是获取日志内存缓冲区的大小,即getLogSize为true,通过调用android::getLogSize函数实现:
- /*returnsthetotalsizeofthelog'sringbuffer*/
- staticintgetLogSize(intlogfd)
- {
- returnioctl(logfd,LOGGER_GET_LOG_BUF_SIZE);
- }
接着验证日志缓冲区可读内容的大小,即调用android::getLogReadableSize函数:
- /*returnsthereadablesizeofthelog'sringbuffer(thatis,amountofthelogconsumed)*/
- staticintgetLogReadableSize(intlogfd)
- {
- returnioctl(logfd,LOGGER_GET_LOG_LEN);
- }
接下去的printf语句,就是输出日志缓冲区的大小以及可读日志的大小到控制台去了。
继续看下看代码,如果执行logcat命令的目的是清空日志或者获取日志的大小信息,则现在就完成使命了,可以退出程序了:
- if(getLogSize){
- return0;
- }
- if(clearLog){
- return0;
- }
- android::readLogLines(devices);
三. 读取日志设备文件。
读取日志设备文件内容的函数是readLogLines函数:
- staticvoidreadLogLines(log_device_t*devices)
- {
- log_device_t*dev;
- intmax=0;
- intret;
- intqueued_lines=0;
- boolsleep=true;
- intresult;
- fd_setreadset;
- for(dev=devices;dev;dev=dev->next){
- if(dev->fd>max){
- max=dev->fd;
- }
- }
- while(1){
- do{
- timevaltimeout={0,5000/*5ms*/};//Ifweoversleepit'sok,i.e.ignoreEINTR.
- 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);
- if(result>=0){
- for(dev=devices;dev;dev=dev->next){
- if(FD_ISSET(dev->fd,&readset)){
- queued_entry_t*entry=newqueued_entry_t();
- /*NOTE:driverguaranteeswereadexactlyonefullentry*/
- ret=read(dev->fd,entry->buf,LOGGER_ENTRY_MAX_LEN);
- if(ret<0){
- if(errno==EINTR){
- deleteentry;
- gotonext;
- }
- if(errno==EAGAIN){
- deleteentry;
- break;
- }
- perror("logcatread");
- exit(EXIT_FAILURE);
- }
- elseif(!ret){
- fprintf(stderr,"read:UnexpectedEOF!\n");
- exit(EXIT_FAILURE);
- }
- entry->entry.msg[entry->entry.len]='\0';
- dev->enqueue(entry);
- ++queued_lines;
- }
- }
- if(result==0){
- //wedidourshorttimeouttrickandthere'snothingnew
- //printeverythingwehaveandwaitformoredata
- 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;
- }
- //thecallerrequestedtojustdumpthelogandexit
- if(g_nonblock){
- exit(0);
- }
- }else{
- //printallthataren'tthelastintheirlist
- sleep=false;
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- }
- }
- next:
- ;
- }
- }
- do{
- timevaltimeout={0,5000/*5ms*/};//Ifweoversleepit'sok,i.e.ignoreEINTR.
- 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);
- for(dev=devices;dev;dev=dev->next){
- if(FD_ISSET(dev->fd,&readset)){
- queued_entry_t*entry=newqueued_entry_t();
- /*NOTE:driverguaranteeswereadexactlyonefullentry*/
- ret=read(dev->fd,entry->buf,LOGGER_ENTRY_MAX_LEN);
- if(ret<0){
- if(errno==EINTR){
- deleteentry;
- gotonext;
- }
- if(errno==EAGAIN){
- deleteentry;
- break;
- }
- perror("logcatread");
- exit(EXIT_FAILURE);
- }
- elseif(!ret){
- fprintf(stderr,"read:UnexpectedEOF!\n");
- exit(EXIT_FAILURE);
- }
- entry->entry.msg[entry->entry.len]='\0';
- dev->enqueue(entry);
- ++queued_lines;
- }
- }
继续进一步处理日志:
- if(result==0){
- //wedidourshorttimeouttrickandthere'snothingnew
- //printeverythingwehaveandwaitformoredata
- 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;
- }
- //thecallerrequestedtojustdumpthelogandexit
- if(g_nonblock){
- exit(0);
- }
- }else{
- //printallthataren'tthelastintheirlist
- sleep=false;
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- }
- staticvoidchooseFirst(log_device_t*dev,log_device_t**firstdev){
- for(*firstdev=NULL;dev!=NULL;dev=dev->next){
- if(dev->queue!=NULL&&(*firstdev==NULL||cmp(dev->queue,(*firstdev)->queue)<0)){
- *firstdev=dev;
- }
- }
- }
- if(g_tail_lines==0||queued_lines<=g_tail_lines){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
如果result > 0,表明有新的日志可读,这时候的处理方式与result == 0的情况不同,因为这时候还有新的日志可读,所以就不能先急着处理之前已经读出来的日志。这里,分两种情况考虑,如果能设置了只显示最新的g_tail_lines条记录,并且当前已经读出来的日志记录条数已经超过g_tail_lines,就要丢弃,剩下的先不处理,等到下次再来处理;如果没有设备显示最新的g_tail_lines条记录,即g_tail_lines == 0,这种情况就和result == 0的情况处理方式一样,先处理所有已经读出的日志记录,再进入下一次循环。希望读者可以好好体会这段代码:
- while(g_tail_lines==0||queued_lines>g_tail_lines){
- chooseFirst(devices,&dev);
- if(dev==NULL||dev->queue->next==NULL){
- break;
- }
- if(g_tail_lines==0){
- printNextEntry(dev);
- }else{
- skipNextEntry(dev);
- }
- --queued_lines;
- }
- staticvoidskipNextEntry(log_device_t*dev){
- maybePrintStart(dev);
- queued_entry_t*entry=dev->queue;
- dev->queue=entry->next;
- deleteentry;
- }
printNextEntry函数处理日志输出,下一节中继续分析。
四.输出日志设备文件的内容。
从前面的分析中看出,最终日志设备文件内容的输出是通过printNextEntry函数进行的:
- staticvoidprintNextEntry(log_device_t*dev){
- maybePrintStart(dev);
- if(g_printBinary){
- printBinary(&dev->queue->entry);
- }else{
- processBuffer(dev,&dev->queue->entry);
- }
- skipNextEntry(dev);
- }
- voidprintBinary(structlogger_entry*buf)
- {
- size_tsize=sizeof(logger_entry)+buf->len;
- intret;
- do{
- ret=write(g_outFD,buf,size);
- }while(ret<0&&errno==EINTR);
- }
- staticvoidprocessBuffer(log_device_t*dev,structlogger_entry*buf)
- {
- intbytesWritten=0;
- interr;
- AndroidLogEntryentry;
- charbinaryMsgBuf[1024];
- if(dev->binary){
- err=android_log_processBinaryLogBuffer(buf,&entry,g_eventTagMap,
- binaryMsgBuf,sizeof(binaryMsgBuf));
- //printf(">>>pri=%dlen=%dmsg='%s'\n",
- //entry.priority,entry.messageLen,entry.message);
- }else{
- err=android_log_processLogBuffer(buf,&entry);
- }
- if(err<0){
- gotoerror;
- }
- 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("outputerror");
- exit(-1);
- }
- }
- bytesWritten=android_log_printLogLine(g_logformat,g_outFD,&entry);
- if(bytesWritten<0){
- perror("outputerror");
- exit(-1);
- }
- }
- g_outByteCount+=bytesWritten;
- if(g_logRotateSizeKBytes>0
- &&(g_outByteCount/1024)>=g_logRotateSizeKBytes
- ){
- rotateLogs();
- }
- error:
- //fprintf(stderr,"Errorprocessingrecord\n");
- return;
- }
当dev->binary为true,日志记录项是二进制形式,不同于我们在Android日志系统驱动程序Logger源代码分析一文中提到的常规格式:
struct logger_entry | priority | tag | msg
这里我们不关注这种情况,有兴趣的读者可以自已分析,android_log_processBinaryLogBuffer函数定义在system/core/liblog/logprint.c文件中,它的作用是将一条二进制形式的日志记录转换为ASCII形式,并保存在entry参数中,它的原型为:
- /**
- *ConvertabinarylogentrytoASCIIform.
- *
- *ForconveniencewemimictheprocessLogBufferAPI.Thereisno
- *pre-definedoutputlengthforthebinarydata,sincewe'refreetoformat
- *ithoweverwechoose,whichmeanswecan'treallyuseafixed-sizebuffer
- *here.
- */
- intandroid_log_processBinaryLogBuffer(structlogger_entry*buf,
- AndroidLogEntry*entry,constEventTagMap*map,char*messageBuf,
- intmessageBufLen);
- typedefstructAndroidLogEntry_t{
- time_ttv_sec;
- longtv_nsec;
- android_LogPrioritypriority;
- pid_tpid;
- pthread_ttid;
- constchar*tag;
- size_tmessageLen;
- constchar*message;
- }AndroidLogEntry;
- /*
- *Androidlogpriorityvalues,inascendingpriorityorder.
- */
- typedefenumandroid_LogPriority{
- ANDROID_LOG_UNKNOWN=0,
- ANDROID_LOG_DEFAULT,/*onlyforSetMinPriority()*/
- ANDROID_LOG_VERBOSE,
- ANDROID_LOG_DEBUG,
- ANDROID_LOG_INFO,
- ANDROID_LOG_WARN,
- ANDROID_LOG_ERROR,
- ANDROID_LOG_FATAL,
- ANDROID_LOG_SILENT,/*onlyforSetMinPriority();mustbelast*/
- }android_LogPriority;
- /**
- *Splitsawire-formatbufferintoanAndroidLogEntry
- *entryallocatedbycaller.Pointerswillpointdirectlyintobuf
- *
- *Returns0onsuccessand-1oninvalidwireformat(entrywillbe
- *inunspecifiedstate)
- */
- intandroid_log_processLogBuffer(structlogger_entry*buf,
- AndroidLogEntry*entry)
- {
- size_ttag_len;
- entry->tv_sec=buf->sec;
- entry->tv_nsec=buf->nsec;
- entry->priority=buf->msg[0];
- entry->pid=buf->pid;
- entry->tid=buf->tid;
- entry->tag=buf->msg+1;
- tag_len=strlen(entry->tag);
- entry->messageLen=buf->len-tag_len-3;
- entry->message=entry->tag+tag_len+1;
- return0;
- }
调用完android_log_processLogBuffer函数后,日志记录的具体信息就保存在本地变量entry中了,接着调用android_log_shouldPrintLine函数来判断这条日志记录是否应该输出。
在分析android_log_shouldPrintLine函数之前,我们先了解数据结构AndroidLogFormat,这个结构体定义在system/core/liblog/logprint.c文件中:
- structAndroidLogFormat_t{
- android_LogPriorityglobal_pri;
- FilterInfo*filters;
- AndroidLogPrintFormatformat;
- };
- typedefstructFilterInfo_t{
- char*mTag;
- android_LogPrioritymPri;
- structFilterInfo_t*p_next;
- }FilterInfo;
- staticAndroidLogFormat*g_logformat;
- g_logformat=android_log_format_new();
回到android_log_shouldPrintLine函数中,它定义在system/core/liblog/logprint.c文件中:
- /**
- *returns1ifthisloglineshouldbeprintedbasedonitspriority
- *andtag,and0ifitshouldnot
- */
- intandroid_log_shouldPrintLine(
- AndroidLogFormat*p_format,constchar*tag,android_LogPrioritypri)
- {
- returnpri>=filterPriForTag(p_format,tag);
- }
- staticandroid_LogPriorityfilterPriForTag(
- AndroidLogFormat*p_format,constchar*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)){
- if(p_curFilter->mPri==ANDROID_LOG_DEFAULT){
- returnp_format->global_pri;
- }else{
- returnp_curFilter->mPri;
- }
- }
- }
- returnp_format->global_pri;
- }
回到processBuffer函数中,如果执行完android_log_shouldPrintLine函数后,表明当前日志记录应当输出,则调用android_log_printLogLine函数来输出日志记录到文件fd中, 这个函数也是定义在system/core/liblog/logprint.c文件中:
- intandroid_log_printLogLine(
- AndroidLogFormat*p_format,
- intfd,
- constAndroidLogEntry*entry)
- {
- intret;
- chardefaultBuffer[512];
- char*outBuffer=NULL;
- size_ttotalLen;
- outBuffer=android_log_formatLogLine(p_format,defaultBuffer,
- sizeof(defaultBuffer),entry,&totalLen);
- if(!outBuffer)
- return-1;
- do{
- ret=write(fd,outBuffer,totalLen);
- }while(ret<0&&errno==EINTR);
- if(ret<0){
- fprintf(stderr,"+++LOG:writefailed(errno=%d)\n",errno);
- ret=0;
- gotodone;
- }
- if(((size_t)ret)<totalLen){
- fprintf(stderr,"+++LOG:writepartial(%dof%d)\n",ret,
- (int)totalLen);
- gotodone;
- }
- done:
- if(outBuffer!=defaultBuffer){
- free(outBuffer);
- }
- returnret;
- }
processBuffer函数的最后,还有一个rotateLogs的操作:
- staticvoidrotateLogs()
- {
- interr;
- //Can'trotatelogsifwe'renotoutputtingtoafile
- if(g_outputFileName==NULL){
- return;
- }
- close(g_outFD);
- for(inti=g_maxRotatedLogs;i>0;i--){
- char*file0,*file1;
- asprintf(&file1,"%s.%d",g_outputFileName,i);
- if(i-1==0){
- asprintf(&file0,"%s",g_outputFileName);
- }else{
- asprintf(&file0,"%s.%d",g_outputFileName,i-1);
- }
- err=rename(file0,file1);
- if(err<0&&errno!=ENOENT){
- perror("whilerotatinglogfiles");
- }
- free(file1);
- free(file0);
- }
- g_outFD=openLogFile(g_outputFileName);
- if(g_outFD<0){
- perror("couldn'topenoutputfile");
- exit(-1);
- }
- g_outByteCount=0;
- }
logfile,logfile.1,logfile.2,logfile.3
当当前输入到logfile文件的日志记录大小g_outByteCount大于等于g_logRotateSizeKBytes时,就要将logfile.2的内容移至logfile.3中,同时将logfile.1的内容移至logfile.2中,同时logfle的内容移至logfile.1中,再重新打开logfile文件进入后续输入。这样做的作用是不至于使得日志文件变得越来越来大,以至于占用过多的磁盘空间,而是只在磁盘上保存一定量的最新的日志记录。这样,旧的日志记录就会可能被新的日志记录所覆盖。
至此,关于Android日志系统源代码,我们就完整地分析完了,其中包括位于内核空间的驱动程序Logger源代码分析,还有位于应用程序框架层和系统运行库层的日志写入操作接口源代码分析和用于日志读取的工具Logcat源代码分析,希望能够帮助读者对Android的日志系统有一个清晰的认识。
相关推荐
《Android系统源代码情景分析》随书光盘内容(源代码) 目录如下: 第1篇 初识Android系统 第1章 准备知识 1.1 Linux内核参考书籍 1.2 Android应用程序参考书籍 1.3 下载、编译和运行Android源代码 ...
Android系统源代码情景分析光盘资料 目录 第1篇初识Android系统 第1章 准备知识................................................................ 2 1.1 Linux内核参考书籍......................................
《Android系统源代码情景分析》随书光盘内容(源代码) 目录如下: 第1篇 初识Android系统 第1章 准备知识 1.1 Linux内核参考书籍 1.2 Android应用程序参考书籍 1.3 下载、编译和运行Android源代码 1.3.1 下载...
OkHttp的请求拦截器,实现请求拦截,打印日志到logcat,链接已经拼接好
Android日志分析工具-V3.6.4与工具源代码. QT C++ 代码开源。 Android常用开发工具Eclipse和Android Studio本身自带有日志查看工具LogCat,一般性使用基本满足要求。但若长期处于Android的深度开发,会发现自带的...
catdea插件旨在将android logcat日志条目与发出它们的源代码相匹配,
雨松MOMO带你做软件 Android软件开发之程序中时时获取logcat日志信息 黄英大家下载阅读 哇咔咔~~
一个静态类,可以直接当做工具来用...直接在程序里面调用,会开启一个新的线程,不会导致假死 详细介绍见 http://www.cnblogs.com/mataojin/
主要介绍了Android studio保存logcat日志到本地的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
Android常用开发工具Eclipse和Android Studio本身自带有日志查看工具LogCat,一般性使用基本满足要求。但若长期处于Android的深度开发,会发现自带的工具内容缓冲区有限,会造成历史数据的丢失,且无法进行信息过滤...
在Android界面上显示和获取Logcat日志输出 在Android界面上显示和获取Logcat日志输出
Android日志工具,支持Logcat输出和文件记录(可自定义大小,默认0.1M),两种显示方式都可以配置是否需要显示
Android开发中LogCat工具的使用;LogCat是Android中一个命令行工具,可以用于得到程序的log信息,本附录讲解其使用方法和其中的一些技巧。
Android的Logcat用于显示系统的调试信息,可在分别以下几个地方查看和调用logcat: 1.eclipse的Debug模式或DDMS模式下的会有一个Logcat窗口,用于显示log日志......
adb logcat查看日志工具,注意:必须有root权限才能查看完整logcat信息。详细介绍请点击http://developer.t-firefly.com/forum.php?mod=viewthread&tid=202&page=1&extra=#pid369
Android开发中LogCat工具的使用
本文指出路径,分析层次但不分析代码,这里还介绍logcat的使用和log_bg服务。 日志系统分层 1.先从驱动开始 linux-3.10/drivers/staging/android/logger.c linux-3.10/drivers/staging/android/logger.h logger...
alogcat android app 读取/显示logcat信息。
shell 脚本抓取android logcat和kernel log,以及删除旧的log,通过prop控制输出规则