1. 程式人生 > IOS開發 >原始碼淺析 - CocoaLumberjack 3.6 之 DDLog

原始碼淺析 - CocoaLumberjack 3.6 之 DDLog

介紹

CocoaLumberjack is a fast & simple,yet powerful & flexible logging framework for Mac and iOS.

先扯一下 lumberjack 這個單詞,對應的就是它的 logo,一位伐木工。 一直不太理解為什麼是用這個單詞,其他語音中也有日誌庫用的這個單詞。最後還是感謝網友提示:log 有代表木頭的意思,所以用 lumberjack 還是非常貼切的,?。

寫這篇文章是最近在使用過程中偶然發現,它居然有這麼多隱藏功能,儘管專案裡引入也有好多年了。接著又看了一下官方提供的 demos, 簡直是驚呆了(PS:也太豐富了吧)。所以本文希望從原始碼來著重來介紹它的一些設計和 ? 。最後會介紹一下它所支援的擴充套件。

Document

作為歷史悠久的 library,它的 document 還是非常詳細的,主要分三個級別:

Architecture

照例,我們先預覽一下類圖,有個大概的印象。

CocoaLumberjackClassDiagram.png

在梳理完腦圖才發現官方其實提供了完整的 UML 圖。不過既然整理了腦圖,那我把它貼在文末。

UML 上直觀感受就是 class 並不多,但是功能確實十分完善,我們一點點來看看。

DDLog

本文預設你是經歷過新手村的,如果對 Lumberjack 的 API 完全不熟悉,請挪步:getting start

核心檔案 DDLog.h 中有宣告瞭最重要的兩個協議 DDLogerDDLogFormatter,而 DDLog class 可以看作是一個 manager 的存在,它管理著所有註冊在案的 loogers 和 formatters。這三個對於正常專案來說已經完全夠用了。我們就從 protocol 著手,最後來說這個 DDLog。

Loggers

A logger is a class that does something with a log message. The lumberjack framework comes with several different loggers. (You can also

create your own.) Loggers such as DDOSLogger can be used to duplicate the functionality of NSLog. And DDFileLogger can be used to write log messages to a log file.

loggers 相關類主要是對 log message 進行加工處理。那麼一條 DDLogMessage 會存有哪些可用資訊呢?

DDLogMessage

Used by the logging primitives. (And the macros use the logging primitives.)

log message 用於記錄日誌原語,它是通過巨集來實現的。logging primitives 是什麼意思呢?可以理解為 log message 儲存了 log 被呼叫時的一系列相關環境的上下文。單詞 primitive 一開始沒看明白,不過計算機中倒是有一個原語的概念(不一定對),可以幫助大家理解這個單詞。

具體存了哪些東西呢?

@interface DDLogMessage : NSObject <NSCopying>
{
    // Direct accessors to be used only for performance
    @public
    NSString *_message;
    DDLogLevel _level;
    DDLogFlag _flag;
    NSInteger _context;
    NSString *_file;
    NSString *_fileName;
    NSString *_function;
    NSUInteger _line;
    id _tag;
    DDLogMessageOptions _options;
    NSDate * _timestamp;
    NSString *_threadID;
    NSString *_threadName;
    NSString *_queueLabel;
    NSUInteger _qos;
}
複製程式碼

這裡通過前置宣告例項變數,這樣呼叫方可以避開 getter 直接訪問變數,來提高訪問效率。當然作者也提供了 readonly 的 @property method。

首先,message、file、function 預設不會執行 copy 操作,如果需要可以通過 DDLogMessageOptions 來控制:

typedef NS_OPTIONS(NSInteger,DDLogMessageOptions){
	 /// Use this to use a copy of the file path
    DDLogMessageCopyFile        = 1 << 0,/// Use this to use a copy of the function name
    DDLogMessageCopyFunction    = 1 << 1,/// Use this to use avoid a copy of the message
    DDLogMessageDontCopyMessage = 1 << 2
};
複製程式碼

我們知道,對於 NSString 的操作需要使用 copy ,以保證我們對它操作時是安全及不可變的。這裡針對 message、file、function 卻不採用 copy,是為了避免不必要的 allocations 開銷。因為 file 和 function 是通過 __FILE__ and __FUNCTION__ 這兩個巨集來獲取的,它們本質上就是一個字元常量,所以可以這麼操作。而 message 正常由 DDlog 內部生成的,Lumberjack 來保證 mesage 不可修改。So 官方提示如下:

If you find need to manually create logMessage objects,there is one thing you should be aware of.

說的就是,當你需要手動生成 log message 的時候需要注意,這三個引數的記憶體修飾操作。

log message 內部實現就比較簡單了,以 message 欄位為例:

BOOL copyMessage = (options & DDLogMessageDontCopyMessage) == 0;
_message = copyMessage ? [message copy] : message;
複製程式碼

另外,就是每個 logMessage 會記錄當前呼叫的 thread & queue 資訊,分別如下:

__uint64_t tid;
if (pthread_threadid_np(NULL,&tid) == 0) {
    _threadID = [[NSString alloc] initWithFormat:@"%llu",tid];
} else {
    _threadID = @"missing threadId";
}
_threadName   = NSThread.currentThread.name;
// Try to get the current queue's label
_queueLabel = [[NSString alloc] initWithFormat:@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
if (@available(macOS 10.10,iOS 8.0,*))
    _qos = (NSUInteger) qos_class_self();
複製程式碼

DDLogLevel

Log levels are used to filter out logs. Used together with flags.

每一條 log mesage 都設定了對應的日誌級別,用於過濾 logs 的。其定義是一個列舉:

typedef NS_ENUM(NSUInteger,DDLogLevel) {
    // No logs
    DDLogLevelOff       = 0,// Error logs only
    DDLogLevelError     = (DDLogFlagError),// Error and warning logs
    DDLogLevelWarning   = (DDLogLevelError   | DDLogFlagWarning),// Error,warning and info logs
    DDLogLevelInfo      = (DDLogLevelWarning | DDLogFlagInfo),warning,info and debug logs
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug),info,debug and verbose logs
    DDLogLevelVerbose   = (DDLogLevelDebug   | DDLogFlagVerbose),// All logs (1...11111)
    DDLogLevelAll       = NSUIntegerMax 
};
複製程式碼

而 loglevel 是由 DDLogFlag 控制,其宣告如下:

typedef NS_OPTIONS(NSUInteger,DDLogFlag) {
    // 0...00001 DDLogFlagError
    DDLogFlagError      = (1 << 0),// 0...00010 DDLogFlagWarning
    DDLogFlagWarning    = (1 << 1),// 0...00100 DDLogFlagInfo
    DDLogFlagInfo       = (1 << 2),// 0...01000 DDLogFlagDebug
    DDLogFlagDebug      = (1 << 3),// 0...10000 DDLogFlagVerbose
    DDLogFlagVerbose    = (1 << 4)
};
複製程式碼

這些就是 DDLog 所預設的 5 種 level,對於新手來說基本夠用了。同時,對於有自定義 level 需求的使用者來說,可以通過結構化的巨集,就能輕鬆實現。詳見 CustomLogLevels.md

其核心是先將預設的 level 清除,然後在進行重新定義:

// First undefine the default stuff we don't want to use.
#undef DDLogError
#undef DDLogWarn
#undef DDLogInfo
#undef DDLogDebug
#undef DDLogVerbose
...
// Now define everything how we want it
#define LOG_FLAG_FATAL   (1 << 0)  // 0...000001
#define LOG_LEVEL_FATAL   (LOG_FLAG_FATAL)  // 0...000001
#define LOG_FATAL   (ddLogLevel & LOG_FLAG_FATAL )
#define DDLogFatal(frmt,...)    SYNC_LOG_OBJC_MAYBE(ddLogLevel,LOG_FLAG_FATAL,frmt,##__VA_ARGS__)
...
複製程式碼

除了對 level 的重定義之外,我們也可以通過對 level 進行擴充套件來滿足我們對需求。由於 lumberjack 使用的是 bitmask 且只預設了 5 個 bit,對應 5 種 log flag。

而 logLevel 作為 Int 型別,意味著對於 32 位的系統而言,預留給我們的 levels 還有 28 bits,因為預設的 level 僅僅佔用了 4 bits。擴充套件空間可以說是綽綽有餘的。官方提供了兩個需要進行擴充套件的場景,詳見:FineGrainedLogging.md

DDLoger

This protocol describes a basic logger behavior.

  • Basically,it can log messages,store a logFormatter plus a bunch of optional behaviors.
  • (i.e. flush,get its loggerQueue,get its name,...
@protocol DDLogger <NSObject>

- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));
@property (nonatomic,strong,nullable) 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 (copy,nonatomic,readonly) DDLoggerName loggerName;

@end
複製程式碼

logMessage 沒啥好說的,logFormatter 會在後面介紹。重點看上面的幾個 optional 方法和引數。

loggerQueue

先看 loggerQueue,由於日誌列印均為非同步操作,所以會為每個 looger 分配一個 dispatch_queue_t。如果 logger 未提供 loggerQueue,那麼 DDLog 為根據你所指定的 loggerName 主動為你生成。

didAddLogger

同樣由於非同步列印日誌的原因,looger 被新增到 loogers 中時也是非同步的過程,didAddLogger 方法就是用於通知 logger 已被成功新增,而這個操作時在 loggerQueue 中完成的。

同樣,didAddLoggerInQueue:willRemoveLogger 目的也是類似。

flush

用於重新整理存在在佇列中還未處理的 log message。比如,database logger 可能通過 I/O buffer 來減少日誌儲存頻率,畢竟磁碟 I/O 是比較耗時的,這種情況下,logger 中可能留有未被及時處理的 log message。

DDLog 會通過 flushLog 來執行 flush 。需要⚠️的是,當應用退出的時候 flushLog 會被自動呼叫。當然,作為開發者我們可以在適當的情況下手動觸發重新整理,正常是不需要手動觸發的。

DDLogFormatter

Formatter allow you to format a log message before the logger logs it.

@protocol DDLogFormatter <NSObject>

@required
- (nullable NSString *)formatLogMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(format(message:));

@optional
- (void)didAddToLogger:(id <DDLogger>)logger;
- (void)didAddToLogger:(id <DDLogger>)logger inQueue:(dispatch_queue_t)queue;
- (void)willRemoveFromLogger:(id <DDLogger>)logger;

@end
複製程式碼

formatLogMessage:

formatter 是可以新增到任何 logger 上的,通過 formatLogMessage: 極大提高了 logging 的自由度。怎麼理解呢?我們可以通過 formatLogMessage: 給 file logger 和 console 返回不同的結果。例如 console 一般系統會自動在 log 前新增時間戳,而當我們寫入 log file 時就需要自行來新增時間。我們還可以通過返回 nil 將其作為 filter 來過濾對應的 log。

didAddToLogger

一個 formatter 可以被新增到多個 logger 上。當 formatter 被新增時,通過這個方法來通知它。該方法是需要保證執行緒安全的,否則可能會出現執行緒安全異常。

同理,didAddToLogger: inQueue 是指在指定佇列中進行 format 操作。

willRemoveFromLogger 則是 formatter 被移除時的通知。

DDLog

The main class,exposes all logging mechanisms,loggers,...

For most of the users,this class is hidden behind the logging functions like DDLogInfo

DDLog 作為 lumberjack 的管理類,負責將使用者的 log 資訊收集後集中排程至不同的 logger 已達到不同的功能,比如 console log 和 file log。因此,作為單例是必須的。我們先來看看它初始化都準備了什麼東西。

Initialize

@interface DDLog ()

@property (nonatomic,strong) NSMutableArray *_loggers;

@end

@implementation DDLog

static dispatch_queue_t _loggingQueue;
static dispatch_group_t _loggingGroup;
static dispatch_semaphore_t _queueSemaphore;
static NSUInteger _numProcessors;
...
複製程式碼

上面幾個均為私有變數,_loggers 自不必說,任何 logger 的新增/刪除都需要在 loggingQueue/loggingThread 中進行的。

_loggingQueue

全域性的 log queue 用於保證 FIFO 的操作順序,所有 logger 會通過它來順序執行各 logger 的 logMessage:

_loggingGroup

由於每個 logger 新增時候都配置了對應的 log queue。因此,loggers 之間的記錄行為是併發執行的。而 dispatch group 可以同步所有 loggers 的操作,確保記錄行為順利完成。

_queueSemaphore

防止所使用的佇列過爆。由於大多數記錄都是非同步操作,因此,可能遭到惡意執行緒大量的增加 log 影響正常的記錄行為。最大限制數為 DDLOG_MAX_QUEUE_SIZE (1000),也就是說當佇列數超過限制,則會主動阻塞執行緒,以待執行佇列降至安全水平。

例如:在大型迴圈中隨意新增日誌語句時會發生過?。

_numProcessors

記錄處理器核心數量,以針對單核情況時進行相應的優化。

作為靜態變數,其初始化則放在 initialize,如下:

+ (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));
    });
}
複製程式碼

上述程式碼中,通過 dispatch_queue_set_specific 為 _loggingQueue 添加了 key:GlobalLoggingQueueIdentityKey 作為標記。之後會在所有的內部方法執行前通過 dispatch_get_specific 獲取 flag 來進行斷言,確保內部方法都是在全域性的 _loggingQueue 中排程的。

接著,我們來看看 DDLog 例項的初始化,僅做了兩件事:

  • _loggers 初始化;
  • 嘗試註冊通知,確保 APP 程式結束前能夠及時將 Logger 中的 message 處理完畢;

由於 lumberjack 支援全平臺以及命令列,這裡的 notificationName 判斷條件相對多一些:

#if TARGET_OS_IOS
    NSString *notificationName = UIApplicationWillTerminateNotification;
#else
    NSString *notificationName = nil;
    // On Command Line Tool apps AppKit may not be available
#if !defined(DD_CLI) && __has_include(<AppKit/NSApplication.h>)
    if (NSApp) {
        notificationName = NSApplicationWillTerminateNotification;
    }
#endif
    if (!notificationName) {
        // If there is no NSApp -> we are running Command Line Tool app.
        // In this case terminate notification wouldn't be fired,so we use workaround.
        __weak __auto_type weakSelf = self;
        atexit_b (^{
            [weakSelf applicationWillTerminate:nil];
        });
    }
#endif /* if TARGET_OS_IOS */
複製程式碼

稍微提一點,命令列中是如何來監聽程式退出?這裡用到了 atexit

The atexit() function registers the given function to be called at program exit,whether via exit(3) or via return from the program's main(). Functions so registered are called in reverse order; no arguments are passed.

就是說,程式在退出時,系統會主動呼叫通過 atexit 註冊的 callbacks,可以註冊多個回撥,按照順序執行。

DDLog 在收到通知後會觸發 flush,這個我們晚一點展開。

if (notificationName) {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationWillTerminate:)
                                                 name:notificationName
                                               object:nil];
}

- (void)applicationWillTerminate:(NSNotification * __attribute__((unused)))notification {
    [self flushLog];
}
複製程式碼

Logger Management

對 logger 的操作主要是新增和刪除。

AddLogger

DDLog 提供了多個新增 logger 的 convince 方法:

+ (void)addLogger:(id <DDLogger>)logger;
- (void)addLogger:(id <DDLogger>)logger;
+ (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level;
- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level;

- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level {
    if (!logger) {
        return;
    }
    dispatch_async(_loggingQueue,^{ @autoreleasepool {
        [self lt_addLogger:logger level:level];
    } });
}
複製程式碼

在放入 _loggingQueue 後,最終走到了 lt_addLogger: level: 方法。這裡的字首 lt 是 lgging thread 的縮寫。在 logger 新增前會檢查去重:

for (DDLoggerNode *node in self._loggers) {
    if (node->_logger == logger && node->_level == level) {
        // Exactly same logger already added,exit
        return;
    }
}
複製程式碼

DDLoggerNode

@interface DDLoggerNode : NSObject
{
    // Direct accessors to be used only for performance
    @public
    id <DDLogger> _logger;
    DDLogLevel _level;
    dispatch_queue_t _loggerQueue;
}

+ (instancetype)nodeWithLogger:(id <DDLogger>)logger
                   loggerQueue:(dispatch_queue_t)loggerQueue
                         level:(DDLogLevel)level;
複製程式碼

私有類,用於關聯 logger、level 和 loggerQueue。

稍微提一下,在 DDLoggerNode 的初始化方法中的,相容了 MRC 的使用。內部使用了一個巨集 OS_OBJECT_USE_OBJC 來區分 GCD 是否支援 ARC。在6.0 之前 GCD 中的物件是不支援 ARC,因此在 6.0 之前 OS_OBJECT_USE_OBJC 是沒有的。

if (loggerQueue) {
    _loggerQueue = loggerQueue;
    #if !OS_OBJECT_USE_OBJC
    dispatch_retain(loggerQueue);
    #endif
}
複製程式碼

接著就是前面所提到的 QueueIdentity 的斷言:

NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey),@"This method should only be run on the logging thread/queue");
複製程式碼

準備 loggerQueue:

dispatch_queue_t loggerQueue = NULL;
if ([logger respondsToSelector:@selector(loggerQueue)]) {
    loggerQueue = logger.loggerQueue;
}

if (loggerQueue == nil) {
    const char *loggerQueueName = NULL;
    if ([logger respondsToSelector:@selector(loggerName)]) {
        loggerQueueName = logger.loggerName.UTF8String;
    }
    loggerQueue = dispatch_queue_create(loggerQueueName,NULL);
}
複製程式碼

這段程式碼,有沒有似曾相識的幹?這是在 DDLogger Protocol 宣告時提到的邏輯。如果 logger 提供了 loggerQueue 則直接使用。否則,通過 loggerName 來建立。

最後就是建立 DDLoggerNode,新增 logger,傳送 didAddLogger 通知。

DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue level:level];
[self._loggers addObject:loggerNode];

if ([logger respondsToSelector:@selector(didAddLoggerInQueue:)]) {
    dispatch_async(loggerNode->_loggerQueue,^{ @autoreleasepool {
        [logger didAddLoggerInQueue:loggerNode->_loggerQueue];
    } });
} else if ([logger respondsToSelector:@selector(didAddLogger)]) {
    dispatch_async(loggerNode->_loggerQueue,^{ @autoreleasepool {
        [logger didAddLogger];
    } });
}
複製程式碼

RemoveLogger

同 addLogger 類似,removeLogger 也提供了例項方法和類方法。類方法通過 sharedInstance 最終收口到例項方法:

- (void)removeLogger:(id <DDLogger>)logger {
    if (!logger) {
        return;
    }
    dispatch_async(_loggingQueue,^{ @autoreleasepool {
        [self lt_removeLogger:logger];
    } });
}
複製程式碼

-[DDLog lt_removeLogger:]

刪除前,照例是 loggingQueue 檢查,然後遍歷獲取 loggerNode:

DDLoggerNode *loggerNode = nil;
for (DDLoggerNode *node in self._loggers) {
    if (node->_logger == logger) {
        loggerNode = node;
        break;
    }
}
複製程式碼

如果 loggerNode 不存在,則提前結束。存在,則會先向 loggerNode 傳送 willRemoveLogger 通知,再移除。

if ([logger respondsToSelector:@selector(willRemoveLogger)]) {
    dispatch_async(loggerNode->_loggerQueue,^{ @autoreleasepool {
        [logger willRemoveLogger];
    } });
}
[self._loggers removeObject:loggerNode];
複製程式碼

DDLog 還提供了 removeAllLoggers 的方法,以一次性清零 loggers,實現同 lt_removeLogger: 類似,這裡不展開了。

Logging

logging 相關方法是 DDLog 的核心,提供三種型別的例項方法,以及分別對應的類方法。我們來看第一個:

+ (void)log:(BOOL)asynchronous
      level:(DDLogLevel)level
       flag:(DDLogFlag)flag
    context:(NSInteger)context
       file:(const char *)file
   function:(nullable const char *)function
       line:(NSUInteger)line
        tag:(nullable id)tag
     format:(NSString *)format,... NS_FORMAT_FUNCTION(9,10);
複製程式碼

熟悉吧,這些引數前面都介紹過了,是構造 log message 所需的關引數。最後一個 C 寫法的可變引數 ... 用於生成 log message string,同樣 DDLog 也提供了它的變種 args:(va_list)argList ,這就是第二種 log 方法。最後一種則是由使用者直接提供 logMessage。

對於 ... 的可變引數的獲取,是通過 c 提供的巨集,程式碼如下:

va_list args;
va_start(args,format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
複製程式碼

-[DDLog queueLogMessage: asynchronously:]

準備好 log message 則開始分發,進行非同步呼叫:

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag {
   dispatch_block_t logBlock = ^{
        dispatch_semaphore_wait(_queueSemaphore,DISPATCH_TIME_FOREVER);
        @autoreleasepool {
            [self lt_log:logMessage];
        }
    };

    if (asyncFlag) {
        dispatch_async(_loggingQueue,logBlock);
    } else if (dispatch_get_specific(GlobalLoggingQueueIdentityKey)) {
        logBlock();
    } else {
        dispatch_sync(_loggingQueue,logBlock);
    }
}
複製程式碼

先忽略 logBlock,看 DDLog 如果處理 loggingQueue 排程,以及如何來避免執行緒死鎖問題。這裡的解決方式絕對需要劃重點。大家經常遇到的主執行緒死鎖,很常見的情況如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(),^{
        NSLog(@"2");
    });
    NSLog(@"3");
}
複製程式碼

這個也是面試會被常常問到的 case。核心點在於,上述程式碼在 main thread 執行了 dispatch_sync 開啟了 main queue 的同步等待。解決方案就有很多種,比如 SDWebImage 中就提供了 dispatch_main_async_safe 來避免該問題。

回到 DDLog,現在大家可以明白在 dispatch_sync 前為何需要多一步 queue identity 的判斷了吧。另外,關於這個問題,github issuse #812 中有比較詳細的論述。

接著看 logBlock,它在執行第一行程式碼時,就開啟了 semaphore_wait 直到可用佇列數小於 maximumQueueSize。通常來說,我們會通過給 queueSize 加鎖的方式來確保可用佇列數的準確性和執行緒安全。但是這裡作者希望,能夠更快速的來獲取新增 log mesage 入佇列的時機,畢竟鎖的開銷比較大。

這種實踐在很多優秀開源庫中都用到了,比如 SDWebImage。

- [DDLog lt_log:]

該方法是將 log message 分配到所以滿足的 logger 手中。開始前照例進行 QueueIdentity 的斷言。接著依據 CPU 核心數是單核或者多核區別對待:

if (_numProcessors > 1) {  ... } else { ... }
複製程式碼
  1. 多核處理器,程式碼如下:
for (DDLoggerNode *loggerNode in self._loggers) {
    if (!(logMessage->_flag & loggerNode->_level)) {
        continue;
    }
    dispatch_group_async(_loggingGroup,loggerNode->_loggerQueue,^{ @autoreleasepool {
        [loggerNode->_logger logMessage:logMessage];
    } });
}
dispatch_group_wait(_loggingGroup,DISPATCH_TIME_FOREVER);
複製程式碼

稍微提一下 DDLog 的設計思路,由於一條 log message 可能會提供給多個不同型別的 logger 處理。例如,一條 log 可能同時需要輸出到終端、寫入到 log file 中、通過 websocket 輸出到瀏覽器方便測試等操作。

首先,通過 logMessage->_flag 過濾掉 level 不匹配的 loggerNode。然後從匹配到的 loggerNode 中取出 loggerQueue 和 logger 呼叫 logMessage:

重點來了,這裡利用 _loggingGroup 將本次的 logMessage: 關聯到 group 中,打包成一個 "事務",以保證每次的 lt_log: 都是順序執行的。而每個 logger 本身都分配了獨立的 loggerQueue,通過這種組合,即保證了 logger 的併發呼叫,又能滿足 queueSize 的限制。

使用 dispatch_group_wait 還有一個目的,就是確保那些執行效果慢的 logger 也能按順序完成呼叫,避免佇列任務過多時,這些 logger 沒能及時完成導致大量的 padding log message 沒有被及時處理。

  1. 對單核處理就比較簡單了,就是第二步不同。不存在 gropu 操作:
dispatch_sync(loggerNode->_loggerQueue,^{ @autoreleasepool {
    [loggerNode->_logger logMessage:logMessage];
} });
複製程式碼

最後,分配完 logger message 後,需要將 _queueSemaphore 加 1:

dispatch_semaphore_signal(_queueSemaphore);
複製程式碼

lt_flush

DDLog 的最後一個方法,會在程式結束前由通知來觸發執行,其實現同 lt_log: 類似:

- (void)lt_flush {
    NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey),@"This method should only be run on the logging thread/queue");

    for (DDLoggerNode *loggerNode in self._loggers) {
        if ([loggerNode->_logger respondsToSelector:@selector(flush)]) {
            dispatch_group_async(_loggingGroup,^{ @autoreleasepool {
                [loggerNode->_logger flush];
            } });
        }
    }
    dispatch_group_wait(_loggingGroup,DISPATCH_TIME_FOREVER);
}
複製程式碼

小結

DDLog 名副其實的 manager,利用了訊號量和 group 高效的完成對 message 的排程,主要做了以下工作:

  1. 管理 logger 的生命週期,並對其新增、刪除操作進行相應通知;
  2. 生成 logMessage 並在執行緒安全的情況下,將其分配到對應的 logger 以加工 message。
  3. 在程式結束後,及時通知 logger 清理 pending 狀態的 message。

Loggers

現在我們來聊聊 logger。DDLog 給我們提供了一個 logger 基類 DDAbstractLogger 以及幾個預設實現。一一來過一下;

DDAbstractLogger

AbstractLogger 宣告如下:

@interface DDAbstractLogger : NSObject <DDLogger>
{
    @public
    id <DDLogFormatter> _logFormatter;
    dispatch_queue_t _loggerQueue;
}

@property (nonatomic,nullable) id <DDLogFormatter> logFormatter;
@property (nonatomic,DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue;
@property (nonatomic,readonly,getter=isOnGlobalLoggingQueue)  BOOL onGlobalLoggingQueue;
@property (nonatomic,getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue;

@end
複製程式碼

先看初始化方法 init

Init

AdstractLogger 預設提供了 loggerQueue 以及當前是否為 loggerQueue 和 全域性 loggingQueue 的 convene 方法。loggerQueue 的初始化是在 init 中完成的,整個 init 也就做了這一件事。

const char *loggerQueueName = NULL;

if ([self respondsToSelector:@selector(loggerName)]) {
    loggerQueueName = self.loggerName.UTF8String;
}

_loggerQueue = dispatch_queue_create(loggerQueueName,NULL);
void *key = (__bridge void *)self;
void *nonNullValue = (__bridge void *)self;
dispatch_queue_set_specific(_loggerQueue,key,NULL);
複製程式碼

同樣先獲取 queueName,這裡預設返回的 loggerNameNSStringFromClass([self class]);

同時,以 self 的地址作為 flag 關聯到 loggerQueue,並用於判斷 onInternalLoggerQueue

LogFormatter

AdstractLogger 最主要的是實現了 logFormatter 的 getter/setter 方法。同時程式碼中賦予了十分詳細的說明,先看看 getter 實現。

Getter

首先是執行緒相關的斷言,確保當前不在 global queue 和 loggerQueue:

NSAssert(![self isOnGlobalLoggingQueue],@"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue],@"MUST access ivar directly,NOT via self.* syntax.");
複製程式碼

接著在 loggingQueue 和 loggerQueue 中獲取 logFormatter:

dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];

__block id <DDLogFormatter> result;

dispatch_sync(globalLoggingQueue,^{
    dispatch_sync(self->_loggerQueue,^{
        result = self->_logFormatter;
    });
});
return result;
複製程式碼

看去一個普通的 formatter 為何需要如此大動干戈,需要層層深入來呢?我們來看一段程式碼:

DDLogVerbose(@"log msg 1");
DDLogVerbose(@"log msg 2");
[logger setFormatter:myFormatter];
DDLogVerbose(@"log msg 3");
複製程式碼

從直覺上,我們希望看到的結果是新設定的 formatter 僅應用在第 3 條 log message 上。然而 DDLog 在整個 logging 過程中卻都是非同步呼叫的。

  1. log message 最終是在單獨的 loggerQueue 中執行的,是由 logger 各自持有的 queue;
  2. 在進入每個 loggerQueue 之前,又要經過一道全域性的 loggingQueue。

So,想要執行緒安全又要符合直覺的話,只能遵循 log message 的腳步,走一遍相關 queue。

需要強調一點,logger在內部最好直接訪問 FORMATTER VARIABLE ,如果需要的話。一旦使用 self. 可能會導致執行緒死鎖。

Setter

同 getter 一致,先斷言,然後依次進入佇列 DDLog.loggingQueue -> self->_loggerQueue 執行 block 開始真正的賦值:

@autoreleasepool {
    if (self->_logFormatter != logFormatter) {
        if ([self->_logFormatter respondsToSelector:@selector(willRemoveFromLogger:)]) {
            [self->_logFormatter willRemoveFromLogger:self];
        }

        self->_logFormatter = logFormatter;

        if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:inQueue:)]) {
            [self->_logFormatter didAddToLogger:self inQueue:self->_loggerQueue];
        } else if ([self->_logFormatter respondsToSelector:@selector(didAddToLogger:)]) {
            [self->_logFormatter didAddToLogger:self];
        }
    }
}
複製程式碼

DDASLLogger

ASLLogger 是對 Apple System Log API 的封裝,我們經常使用的 NSLog 會將其輸出定向到兩個地方:

不過 ASLLogger 在 macosx 10.12 iOS 10.0 已經被廢棄了,取而代之的是 DDOSLoger。ASLLogger 背後使用的 API 是 <asl.h> ,它也提供了幾種 message level

/*! @defineblock Log Message Priority Levels Log levels of the message. */
#define ASL_LEVEL_EMERG   0
#define ASL_LEVEL_ALERT   1
#define ASL_LEVEL_CRIT    2 // DDLogFlagError
#define ASL_LEVEL_ERR     3 // DDLogFlagWarning
#define ASL_LEVEL_WARNING 4 // DDLogFlagInfo,Regular NSLog's level
#define ASL_LEVEL_NOTICE  5 // default
#define ASL_LEVEL_INFO    6
#define ASL_LEVEL_DEBUG   7
複製程式碼

預設情況下 ASL 會過濾 NOTICE 之上的資訊,這也是為何 DDLog 基本也就設定了 5 種日誌級別。

logMessage

logMessage 是每個 logger 處理 log message 的方法。ASLLogger 首先會過濾 filename 為 DDASLLogCapture (主動監聽的系統 log)。然後對 message 進行 formate:

NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message;
複製程式碼

如果 message 存在,生成 aslmsg 通過 asl_send 傳送至 ASL。實現如下:

const char *msg = [message UTF8String];
size_t aslLogLevel; // logMessage->_flag 獲取 ASL_LEVEL_XXX

static char const *const level_strings[] = { "0","1","2","3","4","5","6","7" };

uid_t const readUID = geteuid(); /// the effective user ID of the calling process

char readUIDString[16]; /// formatted output conversion
#ifndef NS_BLOCK_ASSERTIONS
size_t l = (size_t)snprintf(readUIDString,sizeof(readUIDString),"%d",readUID);
#else
snprintf(readUIDString,readUID);
#endif

NSAssert(l < sizeof(readUIDString),@"Formatted euid is too long.");
NSAssert(aslLogLevel < (sizeof(level_strings) / sizeof(level_strings[0])),@"Unhandled ASL log level.");

aslmsg m = asl_new(ASL_TYPE_MSG);
if (m != NULL) {
    if (asl_set(m,ASL_KEY_LEVEL,level_strings[aslLogLevel]) == 0 &&
        asl_set(m,ASL_KEY_MSG,msg) == 0 &&
        asl_set(m,ASL_KEY_READ_UID,readUIDString) == 0 &&
        asl_set(m,kDDASLKeyDDLog,kDDASLDDLogValue) == 0) {
        asl_send(_client,m);
    }
    asl_free(m);
}
複製程式碼

DDOSLogger

蘋果的新一代 logging system os_log,官方提供了比較完整的概述和說明。正是它取代了 ASL,manual 如下:

The unified logging system provides a single,efficient,high performance set of APIs for capturing log messages across all levels of the system. This unified system centralizes the storage of log data in memory and in a data store on disk.

它提供了日誌記錄的中心化儲存。同時 API 也十分簡潔,關於 os_log 有機會在展開。

Init

首先,OSLogger 需要持有一個 log object:

os_log_t os_log_create(const char *subsystem,const char *category);
複製程式碼

subsystem

An identifier string,in reverse DNS notation,that represents the subsystem that’s performing logging,for example,com.your_company.your_subsystem_name. The subsystem is used for categorization and filtering of related log messages,as well as for grouping related logging settings.

category

A category within the specified subsystem. The system uses the category to categorize and filter related log messages,as well as to group related logging settings within the subsystem’s settings. A category’s logging settings override those of the parent subsystem.

順便說一下,os_log 的官方檔案是隻提供了 Swift 說明,OSLog.Category 詳細點此

LogMessage

同樣是過濾 filename 為 DDASLLogCapture 的 log message 和對 log message 的 formatter。os_log 所提供的 API 則十分友好簡潔,每種 os_log_type_t 都提供了對應的方法,使用如下:

__auto_type logger = [self logger];
switch (logMessage->_flag) {
    case DDLogFlagError  :
        os_log_error(logger,"%{public}s",msg);
        break;
    case DDLogFlagWarning:
    case DDLogFlagInfo   :
        os_log_info(logger,msg);
        break;
    case DDLogFlagDebug  :
    case DDLogFlagVerbose:
    default              :
        os_log_debug(logger,msg);
        break;
}
複製程式碼

DDTTYLogger

This class provides a logger for Terminal output or Xcode console output,depending on where you are running your code.

通過它將日誌定向到終端和 Xcode 終端,同時支援彩色。Xcode 支援需要新增 XcodeColors 外掛。TTYLogger 內部的程式碼有上千行。不過所做的事情比較簡單。根據不同終端型別所支援的顏色範圍來將設定的顏色進行適配,最終輸出出來。

關於顏色範圍主要有三種型別:

  • standard shell:僅支援 16 種顏色
  • Terminal.app:可以支援到 256 種顏色
  • xterm colors

具體見 ANSI_escape_code

LogMessage

TTYLogger 支援為每一種 logFlag 配置不同的顏色,然後將 color 與 flag 封裝進 DDTTYLoggerColorProfile 類中,儲存在 _colorProfilesDict 中。logMessage 主要分三步:

  1. 通過 logMessage->_tag 取出 colorProfile;
  2. 將 log message 轉為 c string;
  3. 將 color 寫入 iovec v[iovec_len],最終呼叫 writev(STDERR_FILENO,v,iovec_len); 輸出。

未完待續

以上三種 logger 屬於基本的終端輸出,可用於替代 NSLog。限於篇幅的原因,還有 DDFileLoggerDDAbstractDatabaseLogger 以及各種擴充套件,如 WebSocketLogger 等,未在本篇出現。同時還有一整節的 Formatters 均放下一篇中。

本篇,通過 DDLog 類對 GCD 的使用,看到了 lumberjack 的作者充分利用了 GCD 的特性來達到安全高效的非同步 logging。整個過程中並未使用鎖來解決執行緒安全,算是對 GCD 的很好實踐了。該作者還出品了 CocoaAsyncSocketXMPPFrameworkCocoaHTTPServer 等知名的庫。之後可以慢慢細品。

最後,貼一張整理的腦圖,比較簡單,不喜勿噴。

CocoaLumberjack.png