導(dǎo)語:
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)試起來變得麻煩迈套,NSLog的性能也漸漸成為了瓶頸捐祠,我們也開始有了一些個(gè)性的需求,比如想把某一類日志信息用紅色顯示在控制臺(tái)桑李,比如寫在文件中的日志只寫我們認(rèn)為很關(guān)鍵模塊的部分...... 這時(shí)候NSLog已經(jīng)無法滿足我們的需求踱蛀,我們會(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ì)來滿足這些需求:
正文
想知道DDLog如何此般強(qiáng)大汛骂,我們首先對(duì)DDLog的框架進(jìn)行解析罕模,先看下官方的框架示意圖(已經(jīng)與代碼部分不符合,但不影響理解):
本文將主要對(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)容)之類的處理等行楞,本文不做解析。
下圖是我整理后的圖:
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ì)好基友來標(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語句按順序記錄下來梦鉴,保證每個(gè)logger的添加、移除和level的改變等機(jī)制都能立刻再后面的log語句中生效揭保,如何保證各個(gè)logger中最終記錄的下來的日志是相同的(不會(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才能了解清楚,本文不做解析跨细。