DDLog源碼解析一:框架結(jié)構(gòu)

導(dǎo)語(yǔ):

DDLog,即CocoaLumberjack是iOS開發(fā)用的最多的日志框架,出自大神Robbie Hanson之手(還有諸多知名開源框架如 XMPPFramework、 CocoaAsyncSocket,都是即時(shí)通信領(lǐng)域很基礎(chǔ)應(yīng)用很多的框架)。了解DDLog的源碼將有助于我們更好的輸出代碼中的日志信息,便于定位問題,也能對(duì)我們?cè)跁鴮懽约旱娜罩究蚣芑蛘咂渌K時(shí)有所啟發(fā)。

此系列文章將分為以下幾篇:
- DDLog源碼解析一:框架結(jié)構(gòu)
- DDLog源碼解析二:設(shè)計(jì)初衷
- DDLog源碼解析三:FileLogger

引言:為什么需要DDLog?

我們?cè)趇OS入門階段最早能通過代碼得到的反饋,可能就是打印日志,那時(shí)我們通常會(huì)遇到第一個(gè)朋友:NSLog,這是iOS系統(tǒng)的默認(rèn)打印日志的方式。當(dāng)我們?cè)诔跫?jí)開發(fā)階段NSLog已經(jīng)足夠好,幫我們留下必要信息便于定位問題。

但隨著App復(fù)雜度的增加,調(diào)試起來(lái)變得麻煩,NSLog的性能也漸漸成為了瓶頸,我們也開始有了一些個(gè)性的需求,比如想把某一類日志信息用紅色顯示在控制臺(tái),比如寫在文件中的日志只寫我們認(rèn)為很關(guān)鍵模塊的部分...... 這時(shí)候NSLog已經(jīng)無(wú)法滿足我們的需求,我們會(huì)發(fā)現(xiàn)除了自己造輪子,就只能找輪子了,幸好有DDLog。

需求:DDLog能干什么?

功能上的需求可能包括:

  • 把日志寫到Xcode臺(tái)上
  • 把日志寫到文件里
  • 把日志寫到iOS系統(tǒng)日志中
  • 把日志規(guī)定多個(gè)級(jí)別,我們的日志可以歸類到不同級(jí)別;
  • 根據(jù)日志級(jí)別,我們可以只輸出某個(gè)級(jí)別的日志;
  • 根據(jù)日志級(jí)別,我們可以對(duì)某些級(jí)別日志加顏色顯示;
  • 對(duì)某個(gè)類設(shè)定日志級(jí)別;
  • ......

但是,具備這些功能后,我們可能就要關(guān)注三個(gè)指標(biāo):
準(zhǔn)確!
快速!
安全!
準(zhǔn)確是最基本的,我們要保證日志如實(shí)的記錄我們記錄的東西,內(nèi)容和日志順序、時(shí)間等都是正確的; 快速也是比較重要的點(diǎn),試想我們的高清視頻通話的功能在通話時(shí),如果實(shí)時(shí)打印出很多信息并且寫到文件中,如果性能不過關(guān),就可能會(huì)影響視頻通話的效果;安全范圍很寬泛,除了記錄內(nèi)容的線程安全外,最直接的就是不對(duì)app造成過大侵犯,比如寫到文件中內(nèi)容過多,將導(dǎo)致app大小劇增,對(duì)于手機(jī)容量有限的用戶將造成很大體驗(yàn)上的影響。

而DDLog的設(shè)計(jì)上考慮了這幾點(diǎn),所以我們有必要解析一下DDLog在哪些方面的設(shè)計(jì)來(lái)滿足這些需求:

正文

想知道DDLog如何此般強(qiáng)大,我們首先對(duì)DDLog的框架進(jìn)行解析,先看下官方的框架示意圖(已經(jīng)與代碼部分不符合,但不影響理解):

image.png

本文將主要對(duì)上圖中幾個(gè)重要的類(DDLog、DDLogger、DDAbstractLogger、DDTTYLogger、DDOSLogger、DDFileLogger、DDASLLogger)及其之間的關(guān)系進(jìn)行分析,DDLog主要是通過四種logger分別提供給開發(fā)者四個(gè)方面日志輸出的能力,對(duì)應(yīng)于上面的順序依次是
DDTTYLogger:寫到Xcode控制臺(tái)、
DDOSLogger:寫到iOS10之后的系統(tǒng)日志、
DDFileLogger:寫到文件中、
DDASLLogger:寫到iOS10之前的系統(tǒng)日志,
而DDLog類是對(duì)這四種logger進(jìn)行管理,統(tǒng)一處理日志的輸出的問題。其余類包含fomatter(自定義輸出日志的格式和內(nèi)容)之類的處理等,本文不做解析。

下圖是我整理后的圖:

image.png

DDLogger

這個(gè)協(xié)議主要定義了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協(xié)議的基類,主要是通過一些屬性和方法,描述子類一些通用的行為和能力:

@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方法中定義了子類都要使用的串行隊(duì)列:

_loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
// loggerQueueName由各個(gè)子類名字構(gòu)成

void *key = (__bridge void *)self;
 void *nonNullValue = (__bridge void *)self;

dispatch_queue_set_specific(_loggerQueue, key, nonNullValue, NULL);

需要注意的是,子類init再調(diào)用這個(gè)基類的init方法時(shí),實(shí)際self是相應(yīng)的子類,這樣就會(huì)根據(jù)不同子類生成不同的_loggerQueue,并且通過dispatch_get_specific和dispatch_queue_set_specific一對(duì)好基友來(lái)標(biāo)識(shí)識(shí)別每個(gè)隊(duì)列。

- (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,管理各個(gè)logger的add和remove,并暴露各種記錄日志的log方法,這一步將在[下一節(jié)](DDLog源碼解析二:線程)詳細(xì)解析,主要是線程的保護(hù)機(jī)制,這里的線程保護(hù)機(jī)制包括并不限于:保證log語(yǔ)句按順序記錄下來(lái),保證每個(gè)logger的添加、移除和level的改變等機(jī)制都能立刻再后面的log語(yǔ)句中生效,如何保證各個(gè)logger中最終記錄的下來(lái)的日志是相同的(不會(huì)發(fā)生某一個(gè)logger的日志比其他的多幾條)......

注意,由于initialize是在類或者其子類的第一個(gè)方法被調(diào)用前調(diào)用,并且只會(huì)調(diào)用一次,在DDLog的類、子類或?qū)嵗锌赡苡玫紻DLog中定義的這些資源,這里DDLog將相關(guān)公用的資源申請(qǐng)放在 類方法 +(void)initialize中,保證DDLog在第一次使用時(shí)就已經(jīng)申請(qǐng)好公用資源。

+ (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);
    });
}

這里申請(qǐng)的_queueSemaphore、_loggingQueue、_loggingGroup都是下一節(jié)將重點(diǎn)分析的部分。

DDFileLogger

DDFileLogger繼承自DDAbstractLogger,在實(shí)例化時(shí)將調(diào)用DDAbstractLogger的init方法,從而得到自己的_loggerQueue,并實(shí)現(xiàn)了自己的logMessage方法和其他文件處理相關(guān)方法,第三節(jié)將具體介紹。

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

DDASLLogger

DDASLLogger繼承自DDAbstractLogger,主要功能是將日志寫到ASL中,代碼邏輯簡(jiǎn)單,但需要了解ASL相關(guān)api才能了解清楚,本文不做解析。

DDOSLogger

DDOSLogger繼承自DDAbstractLogger,主要功能是將日志寫到os_log中,代碼邏輯簡(jiǎn)單,但需要了解os_log相關(guān)api才能了解清楚,本文不做解析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容