導語:
DDLog,即CocoaLumberjack是iOS開發用的最多的日志框架,出自大神Robbie Hanson之手(還有諸多知名開源框架如 XMPPFramework、 CocoaAsyncSocket,都是即時通信領域很基礎應用很多的框架)。了解DDLog的源碼將有助于我們更好的輸出代碼中的日志信息,便于定位問題,也能對我們在書寫自己的日志框架或者其他模塊時有所啟發。
此系列文章將分為以下幾篇:
- DDLog源碼解析一:框架結構
- DDLog源碼解析二:設計初衷
- DDLog源碼解析三:FileLogger
引言:為什么需要DDLog?
我們在iOS入門階段最早能通過代碼得到的反饋,可能就是打印日志,那時我們通常會遇到第一個朋友:NSLog,這是iOS系統的默認打印日志的方式。當我們在初級開發階段NSLog已經足夠好,幫我們留下必要信息便于定位問題。
但隨著App復雜度的增加,調試起來變得麻煩,NSLog的性能也漸漸成為了瓶頸,我們也開始有了一些個性的需求,比如想把某一類日志信息用紅色顯示在控制臺,比如寫在文件中的日志只寫我們認為很關鍵模塊的部分...... 這時候NSLog已經無法滿足我們的需求,我們會發現除了自己造輪子,就只能找輪子了,幸好有DDLog。
需求:DDLog能干什么?
功能上的需求可能包括:
- 把日志寫到Xcode臺上
- 把日志寫到文件里
- 把日志寫到iOS系統日志中
- 把日志規定多個級別,我們的日志可以歸類到不同級別;
- 根據日志級別,我們可以只輸出某個級別的日志;
- 根據日志級別,我們可以對某些級別日志加顏色顯示;
- 對某個類設定日志級別;
- ......
但是,具備這些功能后,我們可能就要關注三個指標:
準確!
快速!
安全!
準確是最基本的,我們要保證日志如實的記錄我們記錄的東西,內容和日志順序、時間等都是正確的; 快速也是比較重要的點,試想我們的高清視頻通話的功能在通話時,如果實時打印出很多信息并且寫到文件中,如果性能不過關,就可能會影響視頻通話的效果;安全范圍很寬泛,除了記錄內容的線程安全外,最直接的就是不對app造成過大侵犯,比如寫到文件中內容過多,將導致app大小劇增,對于手機容量有限的用戶將造成很大體驗上的影響。
而DDLog的設計上考慮了這幾點,所以我們有必要解析一下DDLog在哪些方面的設計來滿足這些需求:
正文
想知道DDLog如何此般強大,我們首先對DDLog的框架進行解析,先看下官方的框架示意圖(已經與代碼部分不符合,但不影響理解):
本文將主要對上圖中幾個重要的類(DDLog、DDLogger、DDAbstractLogger、DDTTYLogger、DDOSLogger、DDFileLogger、DDASLLogger)及其之間的關系進行分析,DDLog主要是通過四種logger分別提供給開發者四個方面日志輸出的能力,對應于上面的順序依次是
DDTTYLogger:寫到Xcode控制臺、
DDOSLogger:寫到iOS10之后的系統日志、
DDFileLogger:寫到文件中、
DDASLLogger:寫到iOS10之前的系統日志,
而DDLog類是對這四種logger進行管理,統一處理日志的輸出的問題。其余類包含fomatter(自定義輸出日志的格式和內容)之類的處理等,本文不做解析。
下圖是我整理后的圖:
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才能了解清楚,本文不做解析。