iOS中日志同步获取NSLog重定向以及其他详解

前言

对于那些做后端开发的工程师来说,看LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是对一些不太容易重现的Bug经常焦头烂额。

我们在真机测试时经常会发现一个难题是无法查看真机的NSLog类型的实时日志,这时候需要RD复现问题来定位当时的日志,以方便查找问题。这个问题在测试中是非常常见的,也是功能测试会花费比较长时间的一个原因。

以下我们讨论下能即时查看日志的几种方案。

NSLog输出到哪里?

在iOS开发中,我们经常会用到NSLog调试,但是我们却不太了解它。在NSLog本质是一个C函数,它的函数声明如下:
FOUNDATION_EXPORT void NSLog(NSString *format, ...)

系统对它的说明是:Logs an error message to the Apple System Log facility.。他是用来输出信息到标准Error控制台上去的,其内部其实是使用Apple System Log的API。在调试阶段,日志会输出到到Xcode中,而在iOS真机上,它会输出到系统的/var/log/syslog这个文件中。

在iOS中,把日志输出到文件中的句柄在unistd.h文件中有定义:

#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */

NSLog输出的是到STDERR_FILENO上,我们可以在iOS中使用c语言输出到的文件的fprintf来验证:

NSLog(@"iOS NSLog");
fprintf (stderr, "%s\n", "fprintf log");

由于fprintf并不会像NSLog那样,在内部调用ASL接口,所以只是单纯的输出信息,并没有添加日期、进程名、进程id等,也不会自动换行。

ASL读取日志

首先我们可以想到的是既然日志写入系统的syslog中,那我们可以直接读取这些日志。从ASL读取日志的核心代码如下:

#import <asl.h>
// 从日志的对象aslmsg中获取我们需要的数据
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage{
 SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
 const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
 if (timestamp) {
  NSTimeInterval timeInterval = [@(timestamp) integerValue];
  const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
  if (nanoseconds) {
   timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
  }
  logMessage.timeInterval = timeInterval;
  logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
 }
 const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
 if (sender) {
  logMessage.sender = @(sender);
 }
 const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
 if (messageText) {
  logMessage.messageText = @(messageText);//NSLog写入的文本内容
 }
 const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
 if (messageID) {
  logMessage.messageID = [@(messageID) longLongValue];
 }
 return logMessage;
}
+ (NSMutableArray<SystemLogMessage *> *)allLogMessagesForCurrentProcess{
 asl_object_t query = asl_new(ASL_TYPE_QUERY);
 // Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
 NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
 asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
 aslresponse response = asl_search(NULL, query);
 aslmsg aslMessage = NULL;
 NSMutableArray *logMessages = [NSMutableArray array];
 while ((aslMessage = asl_next(response))) {
  [logMessages addObject:[SystemLogMessage logMessageFromASLMessage:aslMessage]];
 }
 asl_release(response);
 return logMessages;
}

使用以上方法的好处是不会影响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);
#if BETA_BUILD
 stdout->_flags = 10;
 NSPipe *outPipe = [NSPipe pipe];
 NSFileHandle *pipeOutHandle = [outPipe fileHandleForReading];
 dup2([[outPipe fileHandleForWriting] fileDescriptor], STDOUT_FILENO);
 [pipeOutHandle readInBackgroundAndNotify];
 stderr->_flags = 10;
 NSPipe *errPipe = [NSPipe pipe];
 NSFileHandle *pipeErrHandle = [errPipe fileHandleForReading];
 dup2([[errPipe fileHandleForWriting] fileDescriptor], STDERR_FILENO);
 [pipeErrHandle readInBackgroundAndNotify];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectOutNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeOutHandle];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectErrNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeErrHandle];
#endif
}
-(void)recoverStandardOutput{
#if BETA_BUILD
 dup2(self.outFd, STDOUT_FILENO);
 dup2(self.errFd, STDERR_FILENO);
 [[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
}
// 重定向之后的NSLog输出
- (void)redirectOutNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
 NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
 NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
 // YOUR CODE HERE... 保存日志并上传或展示
#endif
 [[nf object] readInBackgroundAndNotify];
}
// 重定向之后的错误输出
- (void)redirectErrNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
 NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
 NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
 // YOUR CODE HERE... 保存日志并上传或展示
#endif
 [[nf object] readInBackgroundAndNotify];
}

文件重定向

另一种重定向的方式是利用c语言的freopen函数进行重定向,将写往stderr的内容重定向到我们制定的文件中去,一旦执行了上述代码那么在这个之后的NSLog将不会在控制台显示了,会直接输出在指定的文件中。

在模拟器中,我们可以使用终端的tail命令(tail -f xxx.log)对这个文件进行实时查看,就如同我们在Xcode的输出窗口中看到的那样,你还可以结合grep命令进行实时过滤查看,非常方便在大量的日志信息中迅速定位到我们要的日志信息。

FILE * freopen ( const char * filename, const char * mode, FILE * stream );

具体代码如下:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/xxx.log"];
//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);

这样我们就可以把可获取的日志文件发送给服务端或者通过itunes共享出来。但是由于iOS严格的沙盒机制,我们无法知道stderr原来的文件路径,也无法直接使用沙盒外的文件,所以freopen无法重定向回去,只能使用第1点所述的dup和dup2来实现。

// 重定向
int origin1 = dup(STDERR_FILENO);
FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
// 恢复重定向
dup2(origin1, STDERR_FILENO);

使用GCD的dispatch Source重定向方式

具体代码如下:

- (dispatch_source_t)_startCapturingWritingToFD:(int)fd {
 int fildes[2];
 pipe(fildes); // [0] is read end of pipe while [1] is write end
 dup2(fildes[1], fd); // Duplicate write end of pipe "onto" fd (this closes fd)
 close(fildes[1]); // Close original write end of pipe
 fd = fildes[0]; // We can now monitor the read end of the pipe
 char* buffer = malloc(1024);
 NSMutableData* data = [[NSMutableData alloc] init];
 fcntl(fd, F_SETFL, O_NONBLOCK);
 dispatch_source_t source = 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_t size = read(fd, buffer, 1024);
    if (size <= 0) {
     break;
    }
    [data appendBytes:buffer length:size];
    if (size < 1024) {
     break;
    }
   }
   NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
   //printf("aString = %s",[aString UTF8String]);
   //NSLog(@"aString = %@",aString);
   // Do something
  }
 });
 dispatch_resume(source);
 return source;
}

日志同步/上传

重定向或者存储的数据可以传到服务端或者通过server同步到网页上,就可以更方便的看到这些数据了。

如果想再网页端实时查看日志,可以在App内置一个小型http web服务器。GitHub上开源的项目有GCDWebServer,可以使用该工具,在APP开启webserver服务,并在同一局域网下,使用http://localhost:8080来请求最新日志了。

上传服务端的部分很简单,实现简单的网络请求就可以,这儿不做叙述。

另外在实际项目中,可以设置一个开关来开启或关闭这个重定向,在调试测试的过程中可以打开开关来查看程序当前的日志。

通过以上处理,真机测试中,日志就可以很方便的获取和查看了,这样能节省不少人力和时间成本。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

参考文档

(0)

相关推荐

  • IOS本地日志记录解决方案

    我们在项目中日志记录这块也算是比较重要的,有时候用户程序出什么问题,光靠服务器的日志还不能准确的找到问题 现在一般记录日志有几种方式: 1.使用第三方工具来记录日志,如腾讯的Bugly,它是只把程序的异常日志,程序崩溃日志,以及一些自定义的操作日志上传到Bugly的后台 2.我们把日志记录到本地,在适合的时候再上传到服务器 这里我要介绍的是第二种方法,第一种和第二种可以一起用. 假如现在有下面这样的日志记录要求 1.日志记录在本地 2.日志最多记录N天,N天之前的都需要清理掉 3.日志可以上传到

  • iOS中捕获日志与异常示例详解

    前言 在平时自己调试的时候,可以直接连接电脑,直接在窗口中查看结果.但是在测试人员测试,或者灰度测试的时候,怎么才能拿到日志呢?最先想到的肯定是输出到本地文件,然后在需要的时候进行上传. 分享一段之前找到的方法,下面的代码提供了两个主要功能: – 把日志输出到文件中 – 捕捉异常信息 [解析都写在注释中了] 示例代码 - (void)redirectNSLogToDocumentFolder { //如果已经连接Xcode调试则不输出到文件 //该函数用于检测输出 (STDOUT_FILENO)

  • iOS中控制NSLog输出时机详解

    -(void)saveDEBUGlog{ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = [paths objectAtIndex:0]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormat

  • iOS中日志同步获取NSLog重定向以及其他详解

    前言 对于那些做后端开发的工程师来说,看LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是对一些不太容易重现的Bug经常焦头烂额. 我们在真机测试时经常会发现一个难题是无法查看真机的NSLog类型的实时日志,这时候需要RD复现问题来定位当时的日志,以方便查找问题.这个问题在测试中是非常常见的,也是功能测试会花费比较长时间的一个原因. 以下我们讨论下能即时查看日志的几种方案. NSLog输出到哪里? 在i

  • iOS中关于信鸽推送的使用demo详解

    最近在看推送方面的知识,用的是信鸽推送主要是因为后台用的是信鸽 推送用第三方推送,也就是在客户端建一个广播接收器,当服务器发送消息时发送到信鸽,信鸽再发送一次,广播接受器接受下: 我实现的功能比较简单,当app在运行状态时,会在主页展示一个弹窗展示推送的消息:如果app不在运行状态且service没被销毁就展示默认的通知 那么如何在主页展示弹窗:当广播接受器收到我要的消息时,用观察者模式,收到消息在发送个消息个主界面 官方的Demo连接:http://xg.qq.com/xg/help/ctr_

  • IOS 中动画的暂停与继续播放的详解

    IOS 中动画的暂停与继续播放的详解 在使用动画控制UI的时候,可能会碰到通过手势或其他方式要进行暂停正在进行中的动画,然后再继续.如手指按下时,暂停动画,手指离开时继续动画. 实现原理主要是通过UI的layer进行相关的控制. 暂停动画: - (void)pauselayer:(CALayer *)layer { CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; layer.s

  • iOS实现封装一个获取通讯录的工具类详解

    前言 本文给大家介绍了关于iOS如何封装一个获取通讯录工具类的相关内容,iOS获取通讯录一共有4个framework: AddressBook, AddressBookUI, Contacts, ContactsUI; 其中 AddressBook 和 AddressBookUI 已经被iOS9时 deprecated 了, 而推出了Contacts 和 ContactsUI 取代之. 其中 AddressBookUI 和 ContactsUI 是picker出一个界面提供选择一条联系人信息并且

  • iOS中常见的视图和图片处理示例详解

    前言 众所周知在开发中不可避免的会遇到一些图片和视图的处理,我这里总结的这些只是我遇到的一些,以供下次使用查看.下面话不多说了,来一起看看详细的介绍吧. 图片的旋转 是UIImage的扩展类,直接使用UIImage的对象调用即可 UIImage #import <QuartzCore/QuartzCore.h> #import <Accelerate/Accelerate.h> @implementation UIImage (ImageRotate) -(UIImage *)im

  • python中日志logging模块的性能及多进程详解

    前言 Java 中最通用的日志模块莫过于 Log4j 了,在 python 中,也自带了 logging 模块,该模块的用法其实和 Log4j 类似.日志是记录操作的一种好方式.但是日志,基本都是基于文件的,也就是要写到磁盘上的.这时候,磁盘将会成为一个性能瓶颈.对于普通的服务器硬盘(机械磁盘,非固态硬盘),Python日志的性能瓶颈是多少呢?今天我们就来测一下.下面话不多说,来一起看看详细的介绍: 测试代码如下: #! /usr/bin/env python #coding=utf-8 # =

  • iOS中id类型的理解及底层原理详解

    前言 id:是一种数据类型: id类型被定义为指向对象的指针,这可以从id的定义中看出.id在objc.h中的定义为: typedef struct objc_object { Class isa; } *id; id是一个一个比较灵活的对象指针,并且是一个指向任何一个继承了Object(或者NSObject)类的对象.而在cocoa的开发环境里,NSObject是所有类的根类.所以id可以指向任何一个cocoa的合法对象. Objective-C中的id这种数据类型存在的价值是什么? id是一

  • iOS中nil、Nil、NULL、NSNull详解

    ObjC 里面的几个空值符号经常会差点把我搞死,这些基础的东西一点要弄清楚才行,以提高码农的基本素质. nil nil 是 ObjC 对象的字面空值,对应 id 类型的对象,或者使用 @interface 声明的 ObjC 对象. 例如: NSString *someString = nil; NSURL *someURL = nil; id someObject = nil; if (anotherObject == nil) // do something 定义: // objc.h #if

  • iOS中wkwebView内存泄漏与循环引用问题详解

    前言 现在大多数网络也面加载都会用到wkwebview,之前在使用wkwebview的时候,网上很多的基础教程使用很多只是说了怎么添加Message Handler 但是并没有告诉到家有这个内存泄漏的风险,如果你只是也没内的数据调用你压根都不会发现这个问题.没存泄漏这个问题说大不大,说小不小,严重的话话直接到时app闪退,所以还是得重视起.好下面说一下怎么解决,话不多说了,来一起看看详细的介绍吧 解决方法 1,在做网页端js交互的时候 我们都会这样去添加js [self.customWebVie

  • IOS登录页面动画、转场动画开发详解

    动画效果 需求分析 分析方法 下载这个gif动图,用mac默认的打开方式打开这个gif图(双击图片即可),效果如下 鼠标选中红色箭头所指的位置,然后按住键盘方向键下键,图片会以缓慢的可控的速度播放,便于分析动画的构成. 小tips:macos系统想正常浏览一个gif动图,可以鼠标单击图片后按空格,也可以选择用浏览器打开,gif图会以正常速度播放. 技术点分析 如何生成一个动画让控件执行? 现流行的方式主要有三种: 1.基本动画 2.核心动画 3.三方框架--POP框架(由Facebook开发)

随机推荐