iOS中日志同步获取NSLog重定向以及其他详解
前言
对于那些做后端开发的工程师来说,看LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是对一些不太容易重现的Bug经常焦头烂额。
我们在真机测试时经常会发现一个难题是无法查看真机的NSLog类型的实时日志,这时候需要RD复现问题来定位当时的日志,以方便查找问题。这个问题在测试中是非常常见的,也是功能测试会花费比较长时间的一个原因。
以下我们讨论下能即时查看日志的几种方案。
NSLog输出到哪里?
在iOS开发中,我们经常会用到NSLog调试,但是我们却不太了解它。在NSLog本质是一个C函数,它的函数声明如下:
FOUNDATION_EXPORTvoidNSLog(NSString*format,...)
系统对它的说明是:LogsanerrormessagetotheAppleSystemLogfacility.。他是用来输出信息到标准Error控制台上去的,其内部其实是使用AppleSystemLog的API。在调试阶段,日志会输出到到Xcode中,而在iOS真机上,它会输出到系统的/var/log/syslog这个文件中。
在iOS中,把日志输出到文件中的句柄在unistd.h文件中有定义:
#defineSTDIN_FILENO0/*standardinputfiledescriptor*/ #defineSTDOUT_FILENO1/*standardoutputfiledescriptor*/ #defineSTDERR_FILENO2/*standarderrorfiledescriptor*/
NSLog输出的是到STDERR_FILENO上,我们可以在iOS中使用c语言输出到的文件的fprintf来验证:
NSLog(@"iOSNSLog"); fprintf(stderr,"%s\n","fprintflog");
由于fprintf并不会像NSLog那样,在内部调用ASL接口,所以只是单纯的输出信息,并没有添加日期、进程名、进程id等,也不会自动换行。
ASL读取日志
首先我们可以想到的是既然日志写入系统的syslog中,那我们可以直接读取这些日志。从ASL读取日志的核心代码如下:
#import//从日志的对象aslmsg中获取我们需要的数据 +(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage{ SystemLogMessage*logMessage=[[SystemLogMessagealloc]init]; constchar*timestamp=asl_get(aslMessage,ASL_KEY_TIME); if(timestamp){ NSTimeIntervaltimeInterval=[@(timestamp)integerValue]; constchar*nanoseconds=asl_get(aslMessage,ASL_KEY_TIME_NSEC); if(nanoseconds){ timeInterval+=[@(nanoseconds)doubleValue]/NSEC_PER_SEC; } logMessage.timeInterval=timeInterval; logMessage.date=[NSDatedateWithTimeIntervalSince1970:timeInterval]; } constchar*sender=asl_get(aslMessage,ASL_KEY_SENDER); if(sender){ logMessage.sender=@(sender); } constchar*messageText=asl_get(aslMessage,ASL_KEY_MSG); if(messageText){ logMessage.messageText=@(messageText);//NSLog写入的文本内容 } constchar*messageID=asl_get(aslMessage,ASL_KEY_MSG_ID); if(messageID){ logMessage.messageID=[@(messageID)longLongValue]; } returnlogMessage; } +(NSMutableArray *)allLogMessagesForCurrentProcess{ asl_object_tquery=asl_new(ASL_TYPE_QUERY); //Filterformessagesfromthecurrentprocess.Notethatthisappearstohappenbydefaultondevice,butisrequiredinthesimulator. NSString*pidString=[NSStringstringWithFormat:@"%d",[[NSProcessInfoprocessInfo]processIdentifier]]; asl_set_query(query,ASL_KEY_PID,[pidStringUTF8String],ASL_QUERY_OP_EQUAL); aslresponseresponse=asl_search(NULL,query); aslmsgaslMessage=NULL; NSMutableArray*logMessages=[NSMutableArrayarray]; while((aslMessage=asl_next(response))){ [logMessagesaddObject:[SystemLogMessagelogMessageFromASLMessage:aslMessage]]; } asl_release(response); returnlogMessages; }
使用以上方法的好处是不会影响Xcode控制台的输出,可以用非侵入性的方式来读取日志。
NSLog重定向
另一种方式就是重定向NSLog,这样NSLog就不会写到系统的syslog中了。
dup2重定向
通过重定向,可以直接截取stdout,stderr等标准输出的信息,然后保存在想要存储的位置,上传到服务器或者显示到View上。
要做到重定向,需要通过NSPipe创建一个管道,pipe有读端和写端,然后通过dup2将标准输入重定向到pipe的写端。再通过NSFileHandle监听pipe的读端,最后再处理读出的信息。
之后通过printf或者NSLog写数据,都会写到pipe的写端,同时pipe会将这些数据直接传送到读端,最后通过NSFileHandle的监控函数取出这些数据。
核心代码如下:
-(void)redirectStandardOutput{ //记录标准输出及错误流原始文件描述符 self.outFd=dup(STDOUT_FILENO); self.errFd=dup(STDERR_FILENO); #ifBETA_BUILD stdout->_flags=10; NSPipe*outPipe=[NSPipepipe]; NSFileHandle*pipeOutHandle=[outPipefileHandleForReading]; dup2([[outPipefileHandleForWriting]fileDescriptor],STDOUT_FILENO); [pipeOutHandlereadInBackgroundAndNotify]; stderr->_flags=10; NSPipe*errPipe=[NSPipepipe]; NSFileHandle*pipeErrHandle=[errPipefileHandleForReading]; dup2([[errPipefileHandleForWriting]fileDescriptor],STDERR_FILENO); [pipeErrHandlereadInBackgroundAndNotify]; [[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(redirectOutNotificationHandle:)name:NSFileHandleReadCompletionNotificationobject:pipeOutHandle]; [[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(redirectErrNotificationHandle:)name:NSFileHandleReadCompletionNotificationobject:pipeErrHandle]; #endif } -(void)recoverStandardOutput{ #ifBETA_BUILD dup2(self.outFd,STDOUT_FILENO); dup2(self.errFd,STDERR_FILENO); [[NSNotificationCenterdefaultCenter]removeObserver:self]; #endif } //重定向之后的NSLog输出 -(void)redirectOutNotificationHandle:(NSNotification*)nf{ #ifBETA_BUILD NSData*data=[[nfuserInfo]objectForKey:NSFileHandleNotificationDataItem]; NSString*str=[[NSStringalloc]initWithData:dataencoding:NSUTF8StringEncoding]; //YOURCODEHERE...保存日志并上传或展示 #endif [[nfobject]readInBackgroundAndNotify]; } //重定向之后的错误输出 -(void)redirectErrNotificationHandle:(NSNotification*)nf{ #ifBETA_BUILD NSData*data=[[nfuserInfo]objectForKey:NSFileHandleNotificationDataItem]; NSString*str=[[NSStringalloc]initWithData:dataencoding:NSUTF8StringEncoding]; //YOURCODEHERE...保存日志并上传或展示 #endif [[nfobject]readInBackgroundAndNotify]; }
文件重定向
另一种重定向的方式是利用c语言的freopen函数进行重定向,将写往stderr的内容重定向到我们制定的文件中去,一旦执行了上述代码那么在这个之后的NSLog将不会在控制台显示了,会直接输出在指定的文件中。
在模拟器中,我们可以使用终端的tail命令(tail-fxxx.log)对这个文件进行实时查看,就如同我们在Xcode的输出窗口中看到的那样,你还可以结合grep命令进行实时过滤查看,非常方便在大量的日志信息中迅速定位到我们要的日志信息。
FILE*freopen(constchar*filename,constchar*mode,FILE*stream);
具体代码如下:
NSArray*paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES); NSString*documentsPath=[pathsobjectAtIndex:0]; NSString*loggingPath=[documentsPathstringByAppendingPathComponent:@"/xxx.log"]; //redirectNSLog freopen([loggingPathcStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
这样我们就可以把可获取的日志文件发送给服务端或者通过itunes共享出来。但是由于iOS严格的沙盒机制,我们无法知道stderr原来的文件路径,也无法直接使用沙盒外的文件,所以freopen无法重定向回去,只能使用第1点所述的dup和dup2来实现。
//重定向 intorigin1=dup(STDERR_FILENO); FILE*myFile=freopen([loggingPathcStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr); //恢复重定向 dup2(origin1,STDERR_FILENO);
使用GCD的dispatchSource重定向方式
具体代码如下:
-(dispatch_source_t)_startCapturingWritingToFD:(int)fd{ intfildes[2]; pipe(fildes);//[0]isreadendofpipewhile[1]iswriteend dup2(fildes[1],fd);//Duplicatewriteendofpipe"onto"fd(thisclosesfd) close(fildes[1]);//Closeoriginalwriteendofpipe fd=fildes[0];//Wecannowmonitorthereadendofthepipe char*buffer=malloc(1024); NSMutableData*data=[[NSMutableDataalloc]init]; fcntl(fd,F_SETFL,O_NONBLOCK); dispatch_source_tsource=dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,fd,0,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0)); dispatch_source_set_cancel_handler(source,^{ free(buffer); }); dispatch_source_set_event_handler(source,^{ @autoreleasepool{ while(1){ ssize_tsize=read(fd,buffer,1024); if(size<=0){ break; } [dataappendBytes:bufferlength:size]; if(size<1024){ break; } } NSString*aString=[[NSStringalloc]initWithData:dataencoding:NSUTF8StringEncoding]; //printf("aString=%s",[aStringUTF8String]); //NSLog(@"aString=%@",aString); //Dosomething } }); dispatch_resume(source); returnsource; }
日志同步/上传
重定向或者存储的数据可以传到服务端或者通过server同步到网页上,就可以更方便的看到这些数据了。
如果想再网页端实时查看日志,可以在App内置一个小型httpweb服务器。GitHub上开源的项目有GCDWebServer,可以使用该工具,在APP开启webserver服务,并在同一局域网下,使用http://localhost:8080来请求最新日志了。
上传服务端的部分很简单,实现简单的网络请求就可以,这儿不做叙述。
另外在实际项目中,可以设置一个开关来开启或关闭这个重定向,在调试测试的过程中可以打开开关来查看程序当前的日志。
通过以上处理,真机测试中,日志就可以很方便的获取和查看了,这样能节省不少人力和时间成本。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。
参考文档
- iOSIO重定向
- iOS日志