获取APP中的NSLog日志

本文是<<iOS开发高手课>> 第十五篇学习笔记.

iOS10之前获取 NSLog 的日志

NSLog 其实就是一个 C 函数,函数声明是:

void NSLog(NSString *format, ...);

它的作用是,输出信息到标准的 Error 控制台和系统日志(syslog)中。

在内部实现上,它其实使用的是 ASL(Apple System Logger,是苹果公司自己实现的一套输出日志的接口)的 API,将日志消息直接存储在磁盘上。

如何获取通过 ASL 存放在系统日志中的日志

ASL 会提供接口去查找所有的日志,第三方日志库 CocoaLumberjack 的 DDASLLogCapture 类,提供了实时捕获 NSLog 的方法。DDASLLogCapture 会在 start 方法里开启一个异步全局队列去捕获 ASL 存储的日志。start 方法的代码如下:

+ (void)start {
    ...
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        [self captureAslLogs];
    });
}

在日志被保存到 ASL 的数据库时,syslogd(系统里用于接收分发日志消息的日志守护进程) 会发出一条通知。因为发过来的这一条通知可能会有多条日志,所以还需要先做些合并的工作,将多条日志进行合并。

+ (void)captureAslLogs {
    @autoreleasepool
    {
        /*
使用ASL_KEY_MSG_ID一次查看每条消息,但是没有明显的方法来获取“下一个” ID。 要引导该过程,我们将按时间戳进行搜索,直到看到消息为止。
         */

        struct timeval timeval = {
            .tv_sec = 0
        };
        gettimeofday(&timeval, NULL);
        unsigned long long startTime = (unsigned long long)timeval.tv_sec;
        __block unsigned long long lastSeenID = 0;

        /*
          当syslogd将消息保存到ASL数据库时,它会通过notify API发布kNotifyASLDBUpdate(com.apple.system.logger.message)。     
          有一些合并-当前每秒最多发送两次-但是对此没有书面保证。 无论如何,每个通知可能有多个消息。
           通知不包含有效负载,因此需要搜索消息。
         */
        int notifyToken = 0;  // 可用于通过notify_cancel()取消注册。
/*
注册通知

notify_register_dispatch 的作用是用来注册进程间的系统通知。其中,kNotifyASLDBUpdate 宏表示的就是,日志被保存到 ASL 数据库时发出的跨进程通知,其键值是 com.apple.system.logger.message。

既然是跨进程通知,那么多个 App 之间也是可以进行通知的。不过对于 iPhone 来说,多个 App 同时保活的机会太少,所以一般都是接收系统功能发出的通知。

在 iOS 系统中,类似地把日志保存到 ASL 数据库时发出的通知还有很多种,比如键值是 com.apple.system.lowdiskspace 的 kNotifyVFSLowDiskSpace 宏,该通知是在系统磁盘空间不足时发出的。当捕获到这个通知时,你可以去清理缓存空间,避免发生缓存写入磁盘失败的情况。

更多的跨进程通知宏,你可以在 notify_keys.h 里看到,
#import <notify_keys.h>
*/ 
        notify_register_dispatch(kNotifyASLDBUpdate, &notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)
        {
            // 至少已发布一条消息; 建立搜索查询。
            @autoreleasepool
            {
                aslmsg query = asl_new(ASL_TYPE_QUERY);
                char stringValue[64];

                if (lastSeenID > 0) {
                    snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID);
                    asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC);
                } else {
                    snprintf(stringValue, sizeof stringValue, "%llu", startTime);
                    asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC);
                }
                // 利用进程标识兼容在模拟器情况时其他进程日志无效通知
                [self configureAslQuery:query];

                // 遍历新消息。
                aslmsg msg;
                aslresponse response = asl_search(NULL, query);
                
                while ((msg = asl_next(response)))
                {
                    // 记录日志
                    [self aslMessageReceived:msg];

                    // 跟踪我们所看到的消息。
                    lastSeenID = (unsigned long long)atoll(asl_get(msg, ASL_KEY_MSG_ID));
                }
                asl_release(response);
                asl_free(query);

                if (_cancel) {
                    notify_cancel(token);
                    return;
                }

            }
        });
    }
}

configureAslQuery

+ (void)configureAslQuery:(aslmsg)query {
    const char param[] = "7";  // 7 == ASL_LEVEL_DEBUG, 这就是一切。 我们将依靠常规的DDlog日志级别进行过滤
    // 设置查询
    asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC);

    // 不检索自己的DDASLLogger中日志
    asl_set_query(query, kDDASLKeyDDLog, kDDASLDDLogValue, ASL_QUERY_OP_NOT_EQUAL);
    
// 如果是模拟器,
#if !TARGET_OS_IPHONE || (defined(TARGET_SIMULATOR) && TARGET_SIMULATOR)
    int processId = [[NSProcessInfo processInfo] processIdentifier];
    char pid[16];
    snprintf(pid, sizeof(pid), "%d", processId);
    asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC);
#endif
}

在 captureAslLogs 方法里,处理日志的方法是 aslMessageReceived,入参是 aslmsg 类型,

+ (void)aslMessageReceived:(aslmsg)msg {
/*
由于 aslmsg 类型不是字符串类型,无法直接查看。所以在 aslMessageReceived 方法的开始阶段,会使用 asl_get 方法将其转换为 char 字符串类型。
*/
    const char* messageCString = asl_get( msg, ASL_KEY_MSG );
    if ( messageCString == NULL )
        return;

    DDLogFlag flag;
    BOOL async;
  
  // 获取NSLog等级
    const char* levelCString = asl_get(msg, ASL_KEY_LEVEL);
    switch (levelCString? atoi(levelCString) : 0) {
        // 默认情况下,所有具有ASL_LEVEL_WARNING级别的NSLog
/*
因为 DDLogMessage 类型包含了日志级别,所以转换类型后还需要设置日志的级别。在此获取日志的级别
*/
        case ASL_LEVEL_EMERG    :
        case ASL_LEVEL_ALERT    :
        case ASL_LEVEL_CRIT     : flag = DDLogFlagError;    async = NO;  break;
        case ASL_LEVEL_ERR      : flag = DDLogFlagWarning;  async = YES; break;
        case ASL_LEVEL_WARNING  : flag = DDLogFlagInfo;     async = YES; break;
        case ASL_LEVEL_NOTICE   : flag = DDLogFlagDebug;    async = YES; break;
        case ASL_LEVEL_INFO     :
        case ASL_LEVEL_DEBUG    :
        default                 : flag = DDLogFlagVerbose;  async = YES;  break;
    }
    
    if (!(_captureLevel & flag)) {
        return;
    }

    //  NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding];

//char 字符串会被转换成 NSString 类型,NSString 是 Objective-C 里字符串类型,转成 NSString 更容易在 Objective-C 里使用。
    NSString *message = @(messageCString);

    const char* secondsCString = asl_get( msg, ASL_KEY_TIME );
    const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC );
    NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970;
    double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0;
    NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9);

    NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds];
  /*
因为 CocoaLumberjack 的日志最后都是通过 DDLog:log:message: 方法进行记录的,其中 message 参数的类型是 DDLogMessage,所以 NSString 类型还需要转换成 DDLogMessage 类型。
*/ 
    DDLogMessage *logMessage = [[DDLogMessage alloc] initWithMessage:message
                                                               level:_captureLevel
                                                                flag:flag
                                                             context:0
                                                                file:@"DDASLLogCapture"
                                                            function:nil
                                                                line:0
                                                                 tag:nil
                                                             options:0
                                                           timestamp:timeStamp];
    
    [DDLog log:async message:logMessage];
}

了解了通过 ASL 获取 NSLog 日志的过程。可以直接使用 CocoaLumberjack 这个库通过 [DDASLLogCapture start] 捕获所有 NSLog 的日志。

现在有很多开发者都用 NSLog 来调试。但一般的程序调试,用断点就好了,使用 NSLog 调试,会发生 IO 磁盘操作,当频繁使用 NSLog 时,性能就会变得不好。另外,各团队都使用 NSLog 来调试的话很容易就会刷屏,这样你也没有办法在控制台上快速、准确地找到你自己的调试信息。
而如果需要汇总一段时间的调试日志的话,把这些日志写到一个文件里就好了。这样的话,随便想要怎么看都行,也不会参杂其他人打的日志。

iOS10之后获取NSLog

为了使日志更高效,更有组织,在 iOS 10 之后,使用了新的统一日志系统(Unified Logging System)来记录日志,全面取代 ASL 的方式。以下是官方原话:

Unified logging is available in iOS 10.0 and later, macOS 10.12 and later, tvOS 10.0 and later, and watchOS 3.0 and later, 
and supersedes ASL (Apple System Logger) and the Syslog APIs. 
Historically, log messages were written to specific locations on disk, such as /etc/system.log. 
The unified logging system stores messages in memory and in a data store, rather than writing to text-based log files.

统一日志记录可在iOS 10.0和更高版本,macOS 10.12和更高版本,tvOS 10.0和更高版本以及watchOS 3.0和更高版本中使用,
并取代ASL(Apple系统记录器)和Syslog API。
 历史上,日志消息被写入磁盘上的特定位置,例如/etc/system.log。 
统一日志记录系统将消息存储在内存和数据存储中,而不是写入基于文本的日志文件。

统一日志系统的方式,是把日志集中存放在内存和数据库里,并提供单一、高效和高性能的接口去获取系统所有级别的消息传递。

macOS 10.12 开始使用了统一日志系统,我们通过控制台应用程序或日志命令行工具,就可以查看到日志消息。

但是,新的统一日志系统没有 ASL 那样的接口可以让我们取出全部日志,所以为了兼容新的统一日志系统,你就需要对 NSLog 日志的输出进行重定向。

对 NSLog 进行重定向,我们首先想到的就是采用 Hook 的方式。因为 NSLog 本身就是一个 C 函数,而不是 Objective-C 方法,所以我们就可以使用 fishhook 来完成重定向的工作。


static void (*orig_nslog)(NSString *format, ...);

void redirect_nslog(NSString *format, ...) {
    // 可以在这里先进行自己的处理
    
    // 继续执行原 NSLog
    va_list va;
    va_start(va, format);
    NSLogv(format, va);
    va_end(va);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};
        NSLog(@"try redirect nslog %@,%d",@"is that ok?");
    }
    return

在 redirect_nslog 方法中,你可以先进行自己的处理,比如将日志的输出重新输出到自己的持久化存储系统里,接着调用 NSLogv 方法进行原 NSLog 方法的调用。也可以使用 fishhook 提供的原方法调用方式orig_nslog,进行原 NSLog 方法的调用。上面代码里也已经声明了类 orig_nslog,直接调用即可。

NSLog 最后写文件时的句柄是 STDERR,STDERR 的全称是 standard error,系统错误日志都会通过 STDERR 句柄来记录,所以 NSLog 最终将错误日志进行写操作的时候也会使用 STDERR 句柄,而 dup2 函数是专门进行文件重定向的,那么也就有了另一个不使用 fishhook 还可以捕获 NSLog 日志的方法。你可以使用 dup2 重定向 STDERR 句柄,使得重定向的位置可以由你来控制,关键代码如下:

// path 就是你自定义的重定向输出的文件地址
int fd = open(path, (O_RDWR | O_CREAT), 0644);
dup2(fd, STDERR_FILENO);

CocoaLumberjack

获取 CocoaLumberjack 日志

CocoaLumberjack 主要由 DDLog、DDLoger、DDLogFormatter 和 DDLogMessage 四部分组成,其整体架构如下图所示:

image.png

在这其中,DDLog 是个全局的单例类,会保存 遵守DDLogger 协议的 logger;DDLogFormatter 用来格式化日志的格式;DDLogMessage 是对日志消息的一个封装;DDLogger 协议是由 DDAbstractLogger 实现的。logger 都是继承于 DDAbstractLogger:

  • 日志输出到控制台是通过 DDTTYLogger 实现的;
  • DDASLLogger 就是用来捕获 NSLog 记录到 ASL 数据库的日志;
  • DDAbstractDatabaseLogger 是数据库操作的抽象接口;
  • DDFileLogger 是用来保存日志到文件的,还提供了返回 CocoaLumberjack 日志保存文件路径的方法,使用方法如下:

logDirectory 方法可以获取日志文件的目录路径。有了目录以后,我们就可以获取到目录下所有的 CocoaLumberjack 的日志了,也就达到了我们要获取 CocoaLumberjack 所有日志的目的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容