導(dǎo)語:
DDLog,即CocoaLumberjack是iOS開發(fā)用的最多的日志框架运授,出自大神Robbie Hanson之手(還有諸多知名開源框架如 XMPPFramework、 CocoaAsyncSocket,都是即時通信領(lǐng)域很基礎(chǔ)應(yīng)用很多的框架)。了解DDLog的源碼將有助于我們更好的輸出代碼中的日志信息器联,便于定位問題,也能對我們在書寫自己的日志框架或者其他模塊時有所啟發(fā)婿崭。
此系列文章將分為以下幾篇:
- DDLog源碼解析一:框架結(jié)構(gòu)
- DDLog源碼解析二:設(shè)計初衷
- DDLog源碼解析三:FileLogger
上一節(jié)介紹了框架結(jié)構(gòu)和主要幾個類的內(nèi)容拨拓,本節(jié)將重點關(guān)注DDLog的核心部分,對于線程的考慮:通過一些線程方面的設(shè)計氓栈,以達到上一節(jié)跳提到的“準(zhǔn)確”渣磷、“快速”、“安全”的目的授瘦,試圖去解析DDLog設(shè)計的初衷醋界,下面我將通過逐步引導(dǎo)來切入正題:
如何快速?
- 作為線程方面考慮的最基本原則奥务,我們都應(yīng)該想到日志作為高頻操作物独,由于不需要UIKit方面接口調(diào)用袜硫,一定要放在子線程氯葬。
- 另外,“快速”也相對于NSLog婉陷,上一節(jié)提到NSLog因為要同時輸出日志兩個源(ASL和控制臺帚称,但不支持文件),所以效率肯定低秽澳,我們平時的需求除非正好跟NSLog輸出的兩個源吻合闯睹,否則我們只需要自己對應(yīng)一個源的輸出,例如只輸出到控制臺担神。
- 代碼執(zhí)行的速度楼吃,這里講在后面具體講解DDLog哪些操作可以加速代碼的執(zhí)行。
如何準(zhǔn)確妄讯?
- 因為要保證日志記錄的順序嚴(yán)格準(zhǔn)確的孩锡,我們自然而然地就會想到串行隊列,讓所有想保證順序的操作都在這個隊列中排隊進行亥贸。
- 當(dāng)我們要處理多個輸出源的時候躬窜,我們肯定要將不同的輸出源的日志操作放在不同線程中,因為不同的源記錄的內(nèi)容是各自順序正確即可炕置,互相沒有依賴和耦合荣挨,如果放在一個線程將降低寫日志的速度男韧。
- 另外每個源的記錄內(nèi)容要高度統(tǒng)一,不然會造成誤解或內(nèi)容確實默垄,影響問題的定位此虑,這時我們就要保證各個不同源的線程要同步,不能某個快很多口锭,其他慢很多寡壮,這時候就需要考慮同步的策略,待選的方案可能有@synchronized讹弯、NSLock况既、dispatch_semaphore、NSCondition组民、OSSpinLock等多種棒仍,我們直接站在巨人的肩膀上,dispatch_semaphore是效率最高的臭胜。
如何安全莫其?
(1)線程安全
DDLog在線程安全方面主要就是通過串行隊列來對一些可能多出訪問的資源進行保護,并且通過dispatch_group和信號量dispatch_semaphore來保證多個線程(串行隊列)中間的同步耸三。
(2)文件安全
在文件安全方面乱陡,主要就是不能讓日志文件無限制的增大,影響到整個app甚至系統(tǒng)的使用仪壮,直接影響用戶對app的留存憨颠。這里DDLog有一套時間輪詢和文件變化時的檢查來控制文件,并且周期更新文件的個數(shù)积锅,保證總占用空間及文件個數(shù)符合開發(fā)者的配置爽彤。
總結(jié)
基于以上分析,我們基本摸清了DDLog類在線程方面的設(shè)計的考慮缚陷,下面考慮一下實際可能需要面對的一個簡單場景:在我們記錄日志的過程中可能存在先removeLogger适篙、addLogger,然后進行高頻的日志寫操作箫爷,由于addLogger之后支持三種Logger的寫日志操作嚷节,所以需要支持三種Logger一起執(zhí)行,并且在日志寫之后又addLogger虎锚。
對于上面的場景硫痰,我們將DDlog處理時線程的設(shè)計描繪如下圖:
全局日志隊列
圖中橙色箭頭表示DDLog類中生成的串行隊列,我們稱之為全局日志隊列翁都,用于控制全局的logger的增減碍论、logger的獲取、各個logger的寫日志等操作的排序執(zhí)行柄慰,這里為什么Logger的增減也要跟寫日志放在一個隊列中呢鳍悠? 看下如下的常見DDLog調(diào)用代碼:
// 初始化
DDLog addLogger:[DDTTYLogger sharedInstance]];
DDLog addLogger:[[DDFileLogger alloc] init]];
// 開始打印日志
DDLogInfo(@"log msg 1");
DDLogInfo(@"log msg 2”);
// 更改日志格式后再打印
[logger setFormatter:myFormatter];
DDLogInfo(@"log msg 3");
當(dāng)addLogger的兩句代碼和DDLogInfo在不同線程執(zhí)行時税娜,勢必可能存在一種可能,當(dāng)?shù)谝痪銬DLogInfo執(zhí)行時藏研,并沒有將日志寫到文件中敬矩,這就可能是因為沒有按照語句的順序來執(zhí)行實際的操作。
對于這個隊列主要涉及創(chuàng)建蠢挡、標(biāo)記弧岳、和判斷當(dāng)前是否是這個線程三個方面:
+ (void)initialize
{
static dispatch_once_t DDLogOnceToken;
dispatch_once(&DDLogOnceToken, ^{
NSLogDebug(@"DDLog: Using grand central dispatch");
_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);
// Figure out how many processors are available.
// This may be used later for an optimization on uniprocessor machines.
_numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger) 1);
NSLogDebug(@"DDLog: numProcessors = %@", @(_numProcessors));
});
}
這里注意下,_loggingQueue业踏、_loggingGroup、_queueSemaphore由于在DDLog的類方法和實例方法都要用到勤家,所以需要聲明為靜態(tài)變量腹尖,并且在DDLog這個類第一次使用時就初始化完成,所以這里將初始化的過程放在 + (void)initialize 方法中伐脖。
各個Logger的串行隊列-- “行進有序”
前面已經(jīng)提過热幔,由于各個Logger不耦合無依賴關(guān)系,所以各個Logger講生成各自的串行隊列來進行內(nèi)部的寫日志讼庇、設(shè)置formatter绎巨、flush的順序執(zhí)行,這里設(shè)置formatter由于是各個Logger通用的代碼蠕啄,將其放在基類來執(zhí)行:
// @implementation DDAbstractLogger
- (id <DDLogFormatter>)logFormatter
{
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block id <DDLogFormatter> result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(_loggerQueue, ^{
result = _logFormatter;
});
});
return result;
}
- (void)setLogFormatter:(id <DDLogFormatter>)logFormatter {
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_block_t block = ^{
@autoreleasepool {
if (_logFormatter != logFormatter) {
if ([_logFormatter respondsToSelector:@selector(willRemoveFromLogger:)]) {
[_logFormatter willRemoveFromLogger:self];
}
_logFormatter = logFormatter;
if ([_logFormatter respondsToSelector:@selector(didAddToLogger:inQueue:)]) {
[_logFormatter didAddToLogger:self inQueue:_loggerQueue];
} else if ([_logFormatter respondsToSelector:@selector(didAddToLogger:)]) {
[_logFormatter didAddToLogger:self];
}
}
}
};
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
dispatch_async(globalLoggingQueue, ^{
dispatch_async(_loggerQueue, block);
});
}
這段代碼已經(jīng)很清楚表明上面圖示的意思:各個Logger內(nèi)部的寫日志场勤、設(shè)置formatter、flush都將首先被加到全局日志隊列中排隊介汹,然后再加到各個Logger內(nèi)部的日志串行隊列排隊却嗡,通過這兩個串行隊列來保證順序的正確性。
通過全局日志隊列和logger內(nèi)部隊列 兩個隊列的約束嘹承,從而保證了日志相關(guān)操作真正做到了“行進有序”。
dispatch_group + dispatch_semaphore:“齊頭并進” + “流量控制”
單個Logger內(nèi)部通過串行隊列已經(jīng)保證了日志相關(guān)操作的順序如庭,但我們面臨上面提到的問題----很容易忽略的問題:當(dāng)我們app的進程被殺掉時叹卷,很有可能出現(xiàn)系統(tǒng)日志或者文件中的日志并不如控制臺上完備,丟失了最后時刻的很多重要信息坪它。 這個原因就是因為系統(tǒng)日志和寫文件的串行隊列執(zhí)行速度比控制臺慢很多骤竹,如果進程被殺掉前一時刻有很多日志操作,就會導(dǎo)致系統(tǒng)日志和文件的隊列中有大量排隊中未執(zhí)行的日志操作往毡,在殺掉進程時沒法完全flush到對應(yīng)的源中蒙揣。
DDLog處理這個情況就是通過dispatch_group + dispatch_semaphore這對GCD基友組合,請留意圖中的綠色箭頭和灰色框內(nèi)部的部分:
- 每個綠色箭頭內(nèi)部有三個箭頭表示三種輸出源开瞭,由于處理日志速度不同懒震,三個藍(lán)色箭頭長度不同罩息,所以綠色箭頭代表的dispatch_group就是要控制三者可以并行運行的前提下,一同向前个扰,等三者都執(zhí)行完了瓷炮,才執(zhí)行后面的日志語句。
- 灰色框體內(nèi)有多個排隊中待執(zhí)行的log語句(綠色箭頭)递宅,為了控制待執(zhí)行的語句數(shù)量不會無限制的大娘香,這里使用了信號量來控制總數(shù)量,否則隊列過大就有可能在進程殺掉時办龄,無法全部執(zhí)行完隊列中待執(zhí)行的log語句烘绽。
通過一段代碼來了解下:
for (DDLoggerNode *loggerNode in self._loggers)
{
// skip the loggers that shouldn't write this message based on the log level
if (!(logMessage->_flag & loggerNode->_level)) {
continue;
}
dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
[loggerNode->_logger logMessage:logMessage];
} });
}
dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_queueSemaphore);
其中DDLoggerNode就是含有Logger信息的model,_loggingGroup就是dispatch_group俐填,loggerNode->_loggerQueue就是logger內(nèi)部的日志隊列诀姚。
可見,對于一次logMessage:logMessage語句玷禽,都會將其放在每個logger內(nèi)部的隊列中去執(zhí)行赫段,并且通過dispatch_group_wait來保證這個隊列都完成再向后運行,這里就保證了“齊頭并進”矢赁。
上面代碼中還出現(xiàn)了dispatch_semaphore_signal糯笙,它和dispatch_semaphore_wait是好基友,二者通過對初始化時的信號量分別進行-1和+1操作來保證“流量控制”
_queueSemaphore = dispatch_semaphore_create(1000);
.
.
.
- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag
{
dispatch_semaphore_wait(_queueSemaphore, DISPATCH_TIME_FOREVER);
dispatch_block_t logBlock = ^{
@autoreleasepool {
[self lt_log:logMessage];
}
};
if (asyncFlag) {
dispatch_async(_loggingQueue, logBlock);
} else {
dispatch_sync(_loggingQueue, logBlock);
}
}
綜上撩银,總結(jié)下DDLogInfo這句日志語句最終執(zhí)行代碼的調(diào)用棧:
DDLogInfo
V
V
宏替換
V
V
[DDLog log:]
V
V
[DDLog queueLogMessage:]
V
V
lt_log
V
V
[loggerNode->_logger logMessage:]
宏替換
這里的宏替換给涕,一方面讓代碼更簡潔,減少代碼正文中頻繁的接口調(diào)用和嵌套额获,另一方面的考慮就是前文提到的“快速”够庙,下面我們從宏替換實際工作的過程,看下為什么通過宏替換可以“快速” (DDLegacyMacros.h):
從DDLogInfo的定義開始:
#define DDLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, LOG_LEVEL_DEF, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__)
其中使用可變參數(shù)... 通過后面的VA_ARGS來標(biāo)識抄邀,VA_ARGS前面的##是為了兼容這個位置可能沒有參數(shù)的情況耘眨。LOG_OBJC_MAYBE定義如下:
#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \
LOG_MAYBE(async, lvl, flg, ctx, __PRETTY_FUNCTION__, frmt, ## __VA_ARGS__)
可以看出LOG_OBJC_MAYBE的宏定義中只是插入另一個____PRETTY_FUNCTION__(函數(shù)名)參數(shù)到LOG_MAYBE中,而LOG_MAYBE的定義如下:
#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \
do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0)
使用do{...}while(0)構(gòu)造后的宏定義不會受到大括號境肾、分號等的影響剔难,使{}內(nèi)部的語句按正常意愿執(zhí)行。這里其實參數(shù)未變奥喻,只是替換成LOG_MACRO:
#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \
[DDLog log : isAsynchronous \
level : lvl \
flag : flg \
context : ctx \
file : __FILE__ \
function : fnct \
line : __LINE__ \
tag : atag \
format : (frmt), ## __VA_ARGS__]
這一步就已經(jīng)到了類DDLog的log接口中偶宫。其中FILE表示文件名,LINE 表示行數(shù)环鲤。
綜上纯趋,上面只是打印函數(shù)的宏定義接口之一DDLogInfo的宏替換過程,其他幾個宏定義接口DDLogError、DDLogWarn吵冒、DDLogDebug纯命、DDLogVerbose都有著相似的過程,但部分傳入?yún)?shù)有區(qū)別桦锄,試想下我們?nèi)绻灰厦娴暮晏鎿Q扎附,直接讓開發(fā)者調(diào)用[DDLog log : xxxxxx]方法,那開發(fā)者一定崩潰了结耀,參數(shù)太多了留夜。
所以從易用性角度出發(fā),必然要給開發(fā)者暴露只含有必要參數(shù)的接口图甜。但是如果DDLogError碍粥、DDLogWarn、DDLogDebug黑毅、DDLogVerbose嚼摩、DDLogInfo都分別封裝一個接口,都需要DDLog類中多寫幾次函數(shù)的調(diào)用才能最終調(diào)用到[DDLog log : xxxxxx]方法矿瘦,這種方式相對于當(dāng)前宏替換的方案明顯增加了很多次堆椪砻妫控件的申請、保存返回地址缚去,將形參壓棧潮秘,釋放堆棧等操作,必然降低代碼執(zhí)行的效率易结,所以我們說這種宏替換的方式在預(yù)編譯階段就直接將需要使用的最終接口確定枕荞,使代碼執(zhí)行做到了“快速”,這一點點的提升對于高頻的寫日志操作來說是有必要的搞动。