源碼淺析 - CocoaLumberjack 3.6 之 DDLog

介紹

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

先扯一下 lumberjack 這個單詞,對應的就是它的 logo,一位伐木工。 一直不太理解爲何是用這個單詞,其餘語音中也有日誌庫用的這個單詞。最後仍是感謝網友提示:log 有表明木頭的意思,因此用 lumberjack 仍是很是貼切的,😂。node

寫這篇文章是最近在使用過程當中偶然發現,它竟然有這麼多隱藏功能,儘管項目裏引入也有好多年了。接着又看了一下官方提供的 demos, 簡直是驚呆了(PS:也太豐富了吧)。因此本文但願從源碼來着重來介紹它的一些設計和 🤔 。最後會介紹一下它所支持的擴展。ios

Document

做爲歷史悠久的 library,它的 document 仍是很是詳細的,主要分三個級別:git

Architecture

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

CocoaLumberjackClassDiagram.png

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

UML 上直觀感覺就是 class 並很少,可是功能確實十分完善,咱們一點點來看看。面試

DDLog

本文默認你是經歷過新手村的,若是對 Lumberjack 的 API 徹底不熟悉,請挪步:getting startobjective-c

核心文件 DDLog.h 中有聲明瞭最重要的兩個協議 DDLogerDDLogFormatter,而 DDLog class 能夠看做是一個 manager 的存在,它管理着全部註冊在案的 loogers 和 formatters。這三個對於正常項目來講已經徹底夠用了。咱們就從 protocol 着手,最後來講這個 DDLog。shell

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.macos

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), 
    // Error, warning, info and debug logs
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug), 
    // Error, warning, 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, 0, 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, loggerNode->_loggerQueue, ^{ @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, 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

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, nonNullValue, 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, sizeof(readUIDString), "%d", 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, "%{public}s", msg);
        break;
    case DDLogFlagDebug  :
    case DDLogFlagVerbose:
    default              :
        os_log_debug(logger, "%{public}s", 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
相關文章
相關標籤/搜索