DDLog源码解析一:框架结构

作者: 半岛夏天 | 来源:发表于2018-08-03 15:18 被阅读36次

    导语:

    DDLog,即CocoaLumberjack是iOS开发用的最多的日志框架,出自大神Robbie Hanson之手(还有诸多知名开源框架如 XMPPFrameworkCocoaAsyncSocket,都是即时通信领域很基础应用很多的框架)。了解DDLog的源码将有助于我们更好的输出代码中的日志信息,便于定位问题,也能对我们在书写自己的日志框架或者其他模块时有所启发。

    此系列文章将分为以下几篇:
    - DDLog源码解析一:框架结构
    - DDLog源码解析二:设计初衷
    - DDLog源码解析三:FileLogger

    引言:为什么需要DDLog?

    我们在iOS入门阶段最早能通过代码得到的反馈,可能就是打印日志,那时我们通常会遇到第一个朋友:NSLog,这是iOS系统的默认打印日志的方式。当我们在初级开发阶段NSLog已经足够好,帮我们留下必要信息便于定位问题。

    但随着App复杂度的增加,调试起来变得麻烦,NSLog的性能也渐渐成为了瓶颈,我们也开始有了一些个性的需求,比如想把某一类日志信息用红色显示在控制台,比如写在文件中的日志只写我们认为很关键模块的部分...... 这时候NSLog已经无法满足我们的需求,我们会发现除了自己造轮子,就只能找轮子了,幸好有DDLog。

    需求:DDLog能干什么?

    功能上的需求可能包括:

    • 把日志写到Xcode台上
    • 把日志写到文件里
    • 把日志写到iOS系统日志中
    • 把日志规定多个级别,我们的日志可以归类到不同级别;
    • 根据日志级别,我们可以只输出某个级别的日志;
    • 根据日志级别,我们可以对某些级别日志加颜色显示;
    • 对某个类设定日志级别;
    • ......

    但是,具备这些功能后,我们可能就要关注三个指标:
    准确!
    快速!
    安全!
    准确是最基本的,我们要保证日志如实的记录我们记录的东西,内容和日志顺序、时间等都是正确的; 快速也是比较重要的点,试想我们的高清视频通话的功能在通话时,如果实时打印出很多信息并且写到文件中,如果性能不过关,就可能会影响视频通话的效果;安全范围很宽泛,除了记录内容的线程安全外,最直接的就是不对app造成过大侵犯,比如写到文件中内容过多,将导致app大小剧增,对于手机容量有限的用户将造成很大体验上的影响。

    而DDLog的设计上考虑了这几点,所以我们有必要解析一下DDLog在哪些方面的设计来满足这些需求:

    正文

    想知道DDLog如何此般强大,我们首先对DDLog的框架进行解析,先看下官方的框架示意图(已经与代码部分不符合,但不影响理解):

    image.png

    本文将主要对上图中几个重要的类(DDLog、DDLogger、DDAbstractLogger、DDTTYLogger、DDOSLogger、DDFileLogger、DDASLLogger)及其之间的关系进行分析,DDLog主要是通过四种logger分别提供给开发者四个方面日志输出的能力,对应于上面的顺序依次是
    DDTTYLogger:写到Xcode控制台、
    DDOSLogger:写到iOS10之后的系统日志、
    DDFileLogger:写到文件中、
    DDASLLogger:写到iOS10之前的系统日志,
    而DDLog类是对这四种logger进行管理,统一处理日志的输出的问题。其余类包含fomatter(自定义输出日志的格式和内容)之类的处理等,本文不做解析。

    下图是我整理后的图:

    image.png

    DDLogger

    这个协议主要定义了logger一些通用的行为:

    @protocol DDLogger <NSObject>
    - (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));
    @property (nonatomic, strong) id <DDLogFormatter> logFormatter;
    
    @optional
    - (void)didAddLogger;
    - (void)didAddLoggerInQueue:(dispatch_queue_t)queue;
    - (void)willRemoveLogger;
    - (void)flush;
    @property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;
    @property (nonatomic, readonly) NSString *loggerName;
    @end
    
    

    DDAbstractLogger

    作为遵守了DDLogger协议的基类,主要是通过一些属性和方法,描述子类一些通用的行为和能力:

    @interface DDAbstractLogger : NSObject <DDLogger>
    {
        @public
        id <DDLogFormatter> _logFormatter;
        dispatch_queue_t _loggerQueue;
    }
    
    @property (nonatomic, strong, nullable) id <DDLogFormatter> logFormatter;
    @property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue;
    @property (nonatomic, readonly, getter=isOnGlobalLoggingQueue)  BOOL onGlobalLoggingQueue;
    @property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue;
    @end
    
    

    此基类的通用init方法中定义了子类都要使用的串行队列:

    _loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
    // loggerQueueName由各个子类名字构成
    
    void *key = (__bridge void *)self;
     void *nonNullValue = (__bridge void *)self;
    
    dispatch_queue_set_specific(_loggerQueue, key, nonNullValue, NULL);
    
    

    需要注意的是,子类init再调用这个基类的init方法时,实际self是相应的子类,这样就会根据不同子类生成不同的_loggerQueue,并且通过dispatch_get_specific和dispatch_queue_set_specific一对好基友来标识识别每个队列。

    - (NSString *)loggerName {
        return NSStringFromClass([self class]);
    }
    
    - (BOOL)isOnGlobalLoggingQueue {
        return (dispatch_get_specific(GlobalLoggingQueueIdentityKey) != NULL);
    }
    
    - (BOOL)isOnInternalLoggerQueue {
        void *key = (__bridge void *)self;
    
        return (dispatch_get_specific(key) != NULL);
    }
    
    

    DDLog

    真正的BOSS,管理各个logger的add和remove,并暴露各种记录日志的log方法,这一步将在[下一节](DDLog源码解析二:线程)详细解析,主要是线程的保护机制,这里的线程保护机制包括并不限于:保证log语句按顺序记录下来,保证每个logger的添加、移除和level的改变等机制都能立刻再后面的log语句中生效,如何保证各个logger中最终记录的下来的日志是相同的(不会发生某一个logger的日志比其他的多几条)......

    注意,由于initialize是在类或者其子类的第一个方法被调用前调用,并且只会调用一次,在DDLog的类、子类或实例中可能用到DDLog中定义的这些资源,这里DDLog将相关公用的资源申请放在 类方法 +(void)initialize中,保证DDLog在第一次使用时就已经申请好公用资源。

    + (void)initialize {
        static dispatch_once_t DDLogOnceToken;
    
        dispatch_once(&DDLogOnceToken, ^{        
            _loggingQueue = dispatch_queue_create("cocoa.lumberjack", NULL);
            _loggingGroup = dispatch_group_create();
    
            void *nonNullValue = GlobalLoggingQueueIdentityKey; // Whatever, just not null
            dispatch_queue_set_specific(_loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL);
    
            _queueSemaphore = dispatch_semaphore_create(DDLOG_MAX_QUEUE_SIZE);
    
            _numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger) 1);
        });
    }
    
    

    这里申请的_queueSemaphore、_loggingQueue、_loggingGroup都是下一节将重点分析的部分。

    DDFileLogger

    DDFileLogger继承自DDAbstractLogger,在实例化时将调用DDAbstractLogger的init方法,从而得到自己的_loggerQueue,并实现了自己的logMessage方法和其他文件处理相关方法,第三节将具体介绍。

    @interface DDFileLogger : DDAbstractLogger <DDLogger> {
        DDLogFileInfo *_currentLogFileInfo;
    }
    
    

    DDASLLogger

    DDASLLogger继承自DDAbstractLogger,主要功能是将日志写到ASL中,代码逻辑简单,但需要了解ASL相关api才能了解清楚,本文不做解析。

    DDOSLogger

    DDOSLogger继承自DDAbstractLogger,主要功能是将日志写到os_log中,代码逻辑简单,但需要了解os_log相关api才能了解清楚,本文不做解析。

    相关文章

      网友评论

        本文标题:DDLog源码解析一:框架结构

        本文链接:https://www.haomeiwen.com/subject/fnoyvftx.html