iOS 日志保存

需求:获取 app 运行时输出日志,包含接入的其余 SDK 的输出,方便开发人员在出现问题的时候,获得详细的Log信 息,快速的识别出问题的缘由并修复和优化等。 一番 google 后有两种方案,一种是非侵入性的,不影响 app 链接 xcode 调试时各类信息的输出,一种是重定向,会致使输出信息在调试时看不到,各有优缺点。 1、ASL 直接获取 咱们能够经过官方文档了解到,OC中最多见的NSLog操做会同时将标准的Error输出到控制台和系统日志(syslog)中(C语言的printf 系列函数并不会,swift的printf为了保证性能也只会在模拟器环境中输出)。其内部是使用Apple System Logger(简称ASL)去实现的,ASL是苹果本身实现的用于输出日志到系统日志库的一套API接口,有点相似于SQL操做。在iOS真机设备上,使用ASL记录的 log被缓存在沙盒文件中,直到设备被重启,通常会输出到系统的 /var/log/syslog 文件中。swift

#import <asl.h>

+(NSArray*)console
{
    NSMutableArray *consoleLog = [NSMutableArray array];
    
    aslclient client = asl_open(NULL, NULL, ASL_OPT_STDERR);
    
    aslmsg query = asl_new(ASL_TYPE_QUERY);
    asl_set_query(query, ASL_KEY_MSG, NULL, ASL_QUERY_OP_NOT_EQUAL);
    aslresponse response = asl_search(client, query);
    
    asl_free(query);
    
    aslmsg message;
    while((message = asl_next(response)) != NULL)
    {
        SystemLogMessage *model = [self logMessageFromASLMessage:message];
        [consoleLog addObject:model];
    }
    if (message != NULL) {
        asl_free(message);
    }
    asl_free(response);
    asl_close(client);
    
    return consoleLog;
}

//这个是怎么从日志的对象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;
        NSDateFormatter *format = [NSDateFormatter new];
        format.dateFormat = @"YYYY-MM-dd hh:mm:ss";
        logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
        logMessage.dateTime = [format stringFromDate:logMessage.date];
    }
    
    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;
}
复制代码

可是Apple从iOS 10开始,为了减弱ASL对于日志系统的侵入性,直接废弃掉了ASLlink,致使在iOS 10以后的系统版本中没法使用 ASL相关的API。所以为了可以在iOS 10以后的版本中一样获取日志数据,咱们寻找一种版本兼容的解决方案。 2、重定向 一、文件重定向 利用c语言的 freopen 函数进行重定向,将写往 stderr 的内容重定向到咱们制定的文件中去,一旦执行了上述代码那么在这个以后的NSLog将不会在控制台显示了,会直接输出在指定的文件中。xcode

// 最简单的重定向
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
filePath = [filePath stringByAppendingPathComponent:[AutoTestEngine defaultEngine].uuid];
NSString *loggingPath = [filePath stringByAppendingPathComponent:@"result.log"];

//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
复制代码

二、dup2 重定向 经过 NSPipe 建立一个管道,pipe有读端和写端,而后经过 dup2 将标准输入重定向到pipe的写端。再通 过 NSFileHandle 监听pipe的读端,最后再处理读出的信息。 以后经过 printf 或者 NSLog 写数据,都会写到pipe的写端,同时pipe会将这些数据直接传送到读端,最后经过NSFileHandle的监控函数取出这些数据,这时能够随意处理,将这些数据写入指定文件。缓存

- (void)redirectStandardOutput{
    //记录标准输出及错误流原始文件描述符
    outFd = dup(STDOUT_FILENO);
    errFd = dup(STDERR_FILENO);
    
    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];
    @autoreleasepool {
        CFRunLoopRun();
    }
}

-(void)recoverStandardOutput{
    if (!outFd) {
        return;
    }
    // 恢复至原来路径
    dup2(outFd, STDOUT_FILENO);
    dup2(errFd, STDERR_FILENO);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

// 重定向以后的NSLog输出
- (void)redirectOutNotificationHandle:(NSNotification *)nf
{
    NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    // YOUR CODE HERE...  保存日志并上传或展现
    if (str) {
        [self writefile:str];
        [[nf object] readInBackgroundAndNotify];
    }
}

// 重定向以后的错误输出
- (void)redirectErrNotificationHandle:(NSNotification *)nf
{
    NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    // YOUR CODE HERE...  保存日志并上传或展现
    if (str) {
        [self writefile:str];
        [[nf object] readInBackgroundAndNotify];
    }
}

- (void)writefile:(NSString *)string
{
    NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    filePath = [filePath stringByAppendingPathComponent:[AutoTestEngine defaultEngine].uuid];
    filePath = [filePath stringByAppendingPathComponent:@"result.log"];
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        [@"" writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }
    
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
    
    [fileHandle seekToEndOfFile];  //将节点跳到文件的末尾
    
    NSData* stringData  = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    [fileHandle writeData:stringData]; //追加写入数据
    
    [fileHandle closeFile];
}
复制代码

三、dispatch_source_t 重定向app

- (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
    
    NSMutableData* data = [[NSMutableData alloc] init];
    fcntl(fd, F_SETFL, O_NONBLOCK);
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, _queue);
    dispatch_source_set_cancel_handler(source, ^{
    });
    dispatch_source_set_event_handler(source, ^{
        @autoreleasepool {
            
            size_t estimatedSize = dispatch_source_get_data(source) + 1;
            char *buffer = (char *)malloc(estimatedSize);
            if (buffer) {
                ssize_t actual = read(fd, buffer, (estimatedSize));
                // do something with buffer
                [data appendBytes:buffer length:actual];
                NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                NSLog(@"aString = %@",aString);
                [self writefile:aString];
                free(buffer);
                
                [data resetBytesInRange:NSMakeRange(0, data.length)];
                [data setLength:0];
                //读取完毕后
//                dispatch_source_cancel(source);
            }
        }
    });
    dispatch_resume(source);
    return source;
}
复制代码

在短视频应用中,上面3种重定向方式均有问题,多是短视频app的特殊性,方案1中在app中不生效,方案2在和短视频运行中会致使app卡住,方案3获取到的信息不正确且会卡住,生成文件很大。函数

方案1不生效没法下手,方案2,3尝试解决,查看文档,发现方案2的通知回调必须在消息循环激活中才生效,所以,建立线程,开启消息循环,将监听,读写操做均放在该线程中,解决了同时运行卡住问题,暂时采起方案2来获取app 运行时的输出日志。oop

转载于:https://juejin.im/post/5cf0edd1f265da1b5d5791bepost