开源库信息 CocoaLumberjack
架构概览
1. DDLog
上面是CocoaLumberjack的架构图,从整体来看主要由DDLog 这个是总的入口,用于管理DDLogger,下面是DDLog的一些关键方法与属性:
@property (class , nonatomic , copy , readonly ) NSArray <id <DDLogger>> *allLoggers;@property (class , nonatomic , copy , readonly ) NSArray <DDLoggerInformation *> *allLoggersWithLevel;@property (class , nonatomic , DISPATCH_QUEUE_REFERENCE_TYPE, readonly ) dispatch_queue_t loggingQueue;+ (void )addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level; + (void )removeLogger:(id <DDLogger>)logger; + (void )removeAllLoggers; + (void )flushLog;
2. DDLogger
DDLogger是所有Logger都需要遵循的协议,我们先来看下DDLogger协议:
@protocol DDLogger <NSObject >@property (nonatomic , strong ) id <DDLogFormatter> logFormatter;@property (nonatomic , DISPATCH_QUEUE_REFERENCE_TYPE, readonly ) dispatch_queue_t loggerQueue;@property (nonatomic , readonly ) NSString *loggerName;- (void )didAddLogger; - (void )didAddLoggerInQueue:(dispatch_queue_t )queue; - (void )willRemoveLogger; - (void )logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME (log(message:)); - (void )flush; @end
2.1 DDAbstractLogger
DDAbstractLogger 是所有Logger 的基类,它为我们提供了默认的logger格式化方法以及默认logger队列的创建,并提供了当前logger所使用队列的情况。
@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
2.2 Loggers
DDASLLogger :ASL是Apple System Log的缩写,它用于发送日志到苹果的日志系统,以便它们显示在Console.app上。DDTTYLogger :用于将日志发送到Xcode控制台。DDFileLogger :用于将日志写入到日志文件下。DDOSLogger :用于将日志写入到os_logDDAbstractDatabaseLogger :是数据库日志的基础类,用于方便我们进行扩展。 一般我们如果只需要NSLog类似的功能,只需要添加DDASLLogger和DDTTYLogger Logger,只有要生成日志文件的情况下才需要DDFileLogger
2.3 DDLogMessage
Logger消息类封装:
@interface DDLogMessage : NSObject <NSCopying> { @public NSString *_message; // 具体的消息 DDLogLevel _level; // 消息等级 DDLogFlag _flag; // 等级flag NSInteger _context; // 上下文标记 NSString *_file; // 文件路径 NSString *_fileName; // 文件名 NSString *_function; // 方法名 NSUInteger _line; // logger所在的行数 id _tag; // tag标记 DDLogMessageOptions _options; // 消息选项 NSDate *_timestamp; // 消息时间戳 NSString *_threadID; // 线程id NSString *_threadName; // 线程名 NSString *_queueLabel; // 队列标记 }
2.4 DDLoggerInformation
DDLoggerInformation用于标示单个logger的信息,包括对应的logger以及对应的level
2.5 DDLogFormatter
DDLogFormatter用于序列化某个logger的输出格式,
@protocol DDLogFormatter <NSObject>@required - (NSString * __nullable)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
DDMultiFormatter : 链式log格式化器,可以添加多个格式化器,添加后每个DDLogMessage 都需要经过这个链式log格式化器的每个格式化器处理DDContextWhitelistFilterLogFormatter :提供黑白名单处理,只有在白名单中的logger才会被输出DDDispatchQueueLogFormatter :这个序列化器主要用于方便查看dispatch_queue,它会将dispatch_queue label 替代mach_thread_id作为输出。
源码解析 我们以官方的样例入手进行源码解析:
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; fileLogger.logFormatter = fileFormatter [DDLog addLogger:fileLogger]; DDLogVerbose (@"Verbose" );
DDLogVerbose其实是一个宏定义,我们先来看下这个宏定义
#define DDLogVerbose(frmt , ... ) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, LOG_LEVEL_DEF, LOG_FLAG_VERBOSE, 0, frmt , ##__VA_ARGS__ )
#define LOG_OBJC_MAYBE(async , lvl , flg , ctx , frmt , ... ) \ LOG_MAYBE(async , lvl , flg , ctx , __PRETTY_FUNCTION__ , frmt , ## __VA_ARGS__ ) #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 ) #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__ ]
我们开始分析上面的宏定义:
#define LOG_ASYNC_ENABLED YES #define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED)
根据LOG_ASYNC_ENABLED 以及LOG_ASYNC_VERBOSE的定义我们可以看出默认的LOG_ASYNC_VERBOSE为YES
我们再来看下LOG_OBJC_MAYBE的第二个参数LOG_LEVEL_DEF,我们的等级可以通过两种方式来定义,一种是定义LOG_LEVEL_DEF,这种情况下,就直接使用LOG_LEVEL_DEF的值,一种是不定义LOG_LEVEL_DEF只定义ddLogLevel,这种情况下使用ddLogLevel的值,
#ifndef LOG_LEVEL_DEF #define LOG_LEVEL_DEF ddLogLevel #endif
接下来是第三个参数LOG_FLAG_VERBOSE,它主要用于过滤某个等级的log的消息。
#define LOG_FLAG_VERBOSE DDLogFlagVerbose
在LOG_MAYBE宏中会对LOG_LEVEL_DEF和LOG_FLAG_VERBOSE进行一个并运算,判断当前的LOG_FLAG 是否在LOG_LEVEL_DEF范围内,如果是的话则调用LOG_MACRO输出log。
我们来看下log方法,我们这里不再去层层看消息是怎么拼接的,我们只看最终调用的那个log方法:
- (void)log:(BOOL)asynchronous message: (NSString *)message level: (DDLogLevel)level flag: (DDLogFlag)flag context: (NSInteger)context file: (const char *)file function: (const char *)function line: (NSUInteger)line tag: (id)tag { DDLogMessage *logMessage = [[DDLogMessage alloc] initWithMessage:message level: level flag: flag context: context file: [NSString stringWithFormat:@"%s" , file] function: [NSString stringWithFormat:@"%s" , function] line: line tag: tag options: (DDLogMessageOptions)0 timestamp: nil]; [self queueLogMessage:logMessage asynchronously:asynchronous]; }
log方法中使用调用方法的参数构建出一个DDLogMessage,然后通过queueLogMessage将消息添加到队列,这里需要注意的是:
在队列尺寸小于maximumQueueSize的时候,只是简单地将消息添加到队列,这时候我们不会使用任何锁来锁住操作,但是如果队列尺寸超过了maximumQueueSize那么就会锁住操作。一旦有有log出队列了,被锁住的操作将会被解锁,CocoaLumberjack,使用计数信号量来作为锁,最大的队列尺寸被定义为DDLOG_MAX_QUEUE_SIZE
- (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 { dispatch_sync(_loggingQueue , logBlock ) ; } }
- (void)lt_log:(DDLogMessage *)logMessage { if (_numProcessors > 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); } else { for (DDLoggerNode *loggerNode in self._loggers ) { if (!(logMessage->_flag & loggerNode->_level )) { continue ; } dispatch_sync(loggerNode->_loggerQueue , ^{ @autoreleasepool { [loggerNode->_logger logMessage:logMessage]; } }); } } dispatch_semaphore_signal(_queueSemaphore ); }
lt_log 会首先判断当前环境是否是多核环境,如果是多核处理器环境下,每个logger的log任务都会被加到自己的队列中,并行执行,但是在多核处理器的环境下,这些任务还会被加到同一个组中,并且会等到组内所有的log都执行完毕后往下执行。如果在单核环境下每个logger只是简单地将log任务添加到自己的队列中,然后看是否超过最大队列长度,如果超过了就会等待。直到有任务完成并出队列。
我们先回头看下DDLog的初始化以及logger是怎么添加的。
DDLog 初始化
这部分我们先来看下DDLog长啥样:
DDLog有如下几个重要的变量:
有关DDLog所有的操作都放在_loggingQueue,以保证它的所有操作都串行化,后续所有的addLogger,removeLogger,allLoggers等等都放在这个队列中执行,这里需要注意的是这里的_loggingQueue与每个Logger自身的_loggerQueue不是一个东西。要注意区分。
static dispatch_queue_t _loggingQueue;
每个单独的logger都并行得执行在自己的队列,_loggingGroup用于这些并行队列的同步
static dispatch_group_t _loggingGroup;
为了保证队列不会无限制地增加这里使用了一个信号量来控制队列的长度
static dispatch_semaphore_t _queueSemaphore;
// 处理器的数量
static NSUInteger _numProcessors;
在第一次初始化DDLog的时候会调用initialize,在这里初始化了_loggingQueue,_loggingGroup,_queueSemaphore,_numProcessors。
+ (void )initialize { static dispatch_once_t DDLogOnceToken; dispatch_once (&DDLogOnceToken, ^{ _loggingQueue = dispatch_queue_create("cocoa.lumberjack" , NULL ); _loggingGroup = dispatch_group_create(); void *nonNullValue = GlobalLoggingQueueIdentityKey; dispatch_queue_set_specific(_loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL ); _queueSemaphore = dispatch_semaphore_create(DDLOG_MAX_QUEUE_SIZE); _numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger ) 1 ); }); }
每次初始化的时候都会调用init,但是由于DDLog以单例的形式存在所以init也只会调用一次。在这个方法中主要创建了self._loggers数组,后续添加的logger都存放在这个列表中。然后添加通知,监听applicationWillTerminate,也就是在应用快要退出的时候调用flushLog
- (id)init { if (self = [super init ]) { self ._loggers = [[NSMutableArray alloc] initWithCapacity:4 ]; NSString * notificationName = @"UIApplicationWillTerminateNotification" ; if (notificationName) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (applicationWillTerminate:) name:notificationName object:nil ]; } return self ; } - (void)applicationWillTerminate:(NSNotification * __attribute__((unused)))notification { [self flushLog]; }
DDLog 添加 Logger
addLogger也是DDLog中的方法所以它跑在_loggingQueue队列上:
- (void)addLogger :(id <DDLogger>)logger withLevel :(DDLogLevel)level { dispatch_async (_loggingQueue, ^{ @autoreleasepool { [self lt_addLogger:logger level:level] ; } }); }
- (void )lt_addLogger:(id <DDLogger>)logger level:(DDLogLevel)level { for (DDLoggerNode* node in self ._loggers) { if (node->_logger == logger && node->_level == level) { return ; } } NSAssert (dispatch_get_specific(GlobalLoggingQueueIdentityKey), @"This method should only be run on the logging thread/queue" ); 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 ); } 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]; } }); } }
lt_addLogger中将会以异步的方式将logger添加到DDLog.由于是异步方式添加,所以每个logger被添加到self._loggers中的时候每个logger的****didAddLoggerInQueue:, didAddLogger:****将会被调用,在这里可以做相应的处理。每个logger都拥有一个队列,可以通过两种方式指定:
一种是通过loggerQueue指定:
@property (nonatomic , DISPATCH_QUEUE_REFERENCE_TYPE, readonly ) dispatch_queue_t loggerQueue;
另一种是指定loggerName,就会将loggerName作为队列名称创建logger队列
@property (nonatomic , readonly ) NSString *loggerName;
后续当前logger的log都会通过这个队列来完成log的输出。
Logger 结构
我们在介绍架构的时候详细介绍过Logger的组成,这里简单重新介绍下,CocoaLumberjack 总共有 DDASLLogger,DDTTYLogger,DDFileLogger,DDOSLogger这几种Logger,并且提供了一个抽象的DDAbstractDatabaseLogger用于我们自己根据实际的业务需求进行扩展数据库Logger,这些Logger都继承自DDAbstractLogger。DDAbstractLogger比较简单主要负责每个logger Queue的创建,以及提供LoggerFormater的设置。至于log的输出形式都交给具体的Logger子类实现。
我们这里以比较常用的DDFileLogger 作为重点给大家介绍下Logger的功能。
对于一个文件日志来说它要能够自动管理日志文件,DDFileLogger在日志超过指定时间(age)后删除,大小超过最大日志文件大小的时候将文件打包成achive文件。并且能够自定义日志文件名,以及日志文件log格式:
我们先来看下DDFileLogger的组成:
@interface DDFileLogger : DDAbstractLogger <DDLogger > { DDLogFileInfo *_currentLogFileInfo; } - (instancetype )init; - (instancetype )initWithLogFileManager:(id <DDLogFileManager>)logFileManager NS_DESIGNATED_INITIALIZER ; - (void )willLogMessage NS_REQUIRES_SUPER ; - (void )didLogMessage NS_REQUIRES_SUPER ; - (BOOL )shouldArchiveRecentLogFileInfo:(DDLogFileInfo *)recentLogFileInfo; @property (readwrite , assign ) unsigned long long maximumFileSize; @property (readwrite , assign ) NSTimeInterval rollingFrequency; @property (readwrite , assign , atomic) BOOL doNotReuseLogFiles; @property (strong , nonatomic , readonly ) id <DDLogFileManager> logFileManager; @property (nonatomic , readwrite , assign ) BOOL automaticallyAppendNewlineForCustomFormatters;- (void )rollLogFileWithCompletionBlock:(void (^)(void ))completionBlock NS_SWIFT_NAME (rollLogFile(withCompletion:)); - (id <DDLogFormatter>)logFormatter; @property (nonatomic , readonly , strong ) DDLogFileInfo *currentLogFileInfo;
DDFileLogger有两个重要的对象:DDLogFileManagerDefault,以及DDLogFileFormatterDefault.前者负责日志文件的管理,后者负责日志的输出格式,这两者都可以自己实现来达到自定义的目的:
LogFileManager是所有日志文件都必须遵循的协议,它负责日志文件的创建,删除,日志文件命名等所有日志文件的管理,默认情况下每个日志文件的最大尺寸为1M,超过这个大小,日志文件将会被roll,默认情况下每个日志文件的有效期为24小时,超过24小时也会被roll,默认最多保存5个日志文件,整个日志磁盘配额最大为20M.
unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024 ; NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24 ; NSUInteger const kDDDefaultLogMaxNumLogFiles = 5 ; unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024 ;
@protocol DDLogFileManager <NSObject >@required @property (readwrite , assign , atomic) NSUInteger maximumNumberOfLogFiles;@property (readwrite , assign , atomic) unsigned long long logFilesDiskQuota;@property (nonatomic , readonly , copy ) NSString *logsDirectory;@property (nonatomic , readonly , strong ) NSArray <NSString *> *unsortedLogFilePaths;@property (nonatomic , readonly , strong ) NSArray <NSString *> *unsortedLogFileNames;@property (nonatomic , readonly , strong ) NSArray <DDLogFileInfo *> *unsortedLogFileInfos;@property (nonatomic , readonly , strong ) NSArray <NSString *> *sortedLogFilePaths;@property (nonatomic , readonly , strong ) NSArray <NSString *> *sortedLogFileNames;@property (nonatomic , readonly , strong ) NSArray <DDLogFileInfo *> *sortedLogFileInfos;- (NSString *)createNewLogFile; @optional - (void )didArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME (didArchiveLogFile(atPath:)); - (void )didRollAndArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME (didRollAndArchiveLogFile(atPath:)); @end
我们来看下默认提供的DDLogFileManagerDefault的具体实现:
初始化
- (instancetype )initWithLogsDirectory:(NSString *)aLogsDirectory { if ((self = [super init])) { _maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles; _logFilesDiskQuota = kDDDefaultLogFilesDiskQuota; if (aLogsDirectory) { _logsDirectory = [aLogsDirectory copy ]; } else { _logsDirectory = [[self defaultLogsDirectory] copy ]; } NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew ; [self addObserver:self forKeyPath:NSStringFromSelector (@selector (maximumNumberOfLogFiles)) options:kvoOptions context:nil ]; [self addObserver:self forKeyPath:NSStringFromSelector (@selector (logFilesDiskQuota)) options:kvoOptions context:nil ]; } return self ; }
初始化方法中会指定日志的最大文件数量以及日志文件所占用的最大空间,以及日志文件存储目录。以及使用KVO检测maximumNumberOfLogFiles,logFilesDiskQuota的设置。一旦我们对这两个值进行设置就会触发下面的方法:
- (void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id )object change:(NSDictionary *)change context:(void *)context { NSNumber *old = change[NSKeyValueChangeOldKey ]; NSNumber *new = change[NSKeyValueChangeNewKey ]; if ([old isEqual:new]) { return ; } if ([keyPath isEqualToString:NSStringFromSelector (@selector (maximumNumberOfLogFiles))] || [keyPath isEqualToString:NSStringFromSelector (@selector (logFilesDiskQuota))]) { dispatch_async ([DDLog loggingQueue], ^{ @autoreleasepool { [self deleteOldLogFiles]; } }); } }
在设置最大磁盘空间以及最大文件数的时候会对磁盘空间情况做个扫描,将超过的部分删除。
- (void )deleteOldLogFiles { NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; NSUInteger firstIndexToDelete = NSNotFound ; const unsigned long long diskQuota = self .logFilesDiskQuota; const NSUInteger maxNumLogFiles = self .maximumNumberOfLogFiles; if (diskQuota) { unsigned long long used = 0 ; for (NSUInteger i = 0 ; i < sortedLogFileInfos.count; i++) { DDLogFileInfo *info = sortedLogFileInfos[i]; used += info.fileSize; if (used > diskQuota) { firstIndexToDelete = i; break ; } } } if (maxNumLogFiles) { if (firstIndexToDelete == NSNotFound ) { firstIndexToDelete = maxNumLogFiles; } else { firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles); } } if (firstIndexToDelete != NSNotFound ) { for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) { DDLogFileInfo *logFileInfo = sortedLogFileInfos[i]; [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil ]; } } }
我们来看下是怎么对文件进行排序的?
- (NSArray *)sortedLogFileInfos { return [[self unsortedLogFileInfos] sortedArrayUsingComparator:^NSComparisonResult (DDLogFileInfo * _Nonnull obj1, DDLogFileInfo * _Nonnull obj2) { NSDate *date1 = [NSDate new]; NSDate *date2 = [NSDate new]; NSArray <NSString *> *arrayComponent = [[obj1 fileName] componentsSeparatedByString:@" " ]; if (arrayComponent.count > 0 ) { NSString *stringDate = arrayComponent.lastObject; stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@"" ]; stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@"" ]; date1 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj1 creationDate]; } arrayComponent = [[obj2 fileName] componentsSeparatedByString:@" " ]; if (arrayComponent.count > 0 ) { NSString *stringDate = arrayComponent.lastObject; stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@"" ]; stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@"" ]; date2 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj2 creationDate]; } return [date2 compare:date1 ?: [NSDate new]]; }]; }
- (NSArray *)unsortedLogFileInfos { NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; for (NSString *filePath in unsortedLogFilePaths) { DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath]; [unsortedLogFileInfos addObject:logFileInfo]; } return unsortedLogFileInfos; }
- (NSArray *)unsortedLogFilePaths { NSString *logsDirectory = [self logsDirectory]; NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil ]; NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]]; for (NSString *fileName in fileNames) { if ([self isLogFile:fileName]) { NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; [unsortedLogFilePaths addObject:filePath]; } } return unsortedLogFilePaths; }
- (BOOL )isLogFile:(NSString *)fileName { NSString *appName = [self applicationName]; BOOL hasProperPrefix = [fileName hasPrefix:[appName stringByAppendingString:@" " ]]; BOOL hasProperSuffix = [fileName hasSuffix:@".log" ]; return (hasProperPrefix && hasProperSuffix); }
首先会进入日志目录,对日志目录下对文件进行判断,通过isLogFile判断当前文件是否是日志文件,将日志文件添加到unsortedLogFilePaths,这里的日志文件被定义为”应用名 xxxxxxxx.log”形式的文件,这个定义是可以定制的我们在介绍日志文件的创建的时候会对这部分进行介绍。上面排序就是扫描日志文件夹下的符合日志格式的文件,将其添加到数组,再对数组的文件根据创建时间进行排序。
我们再来看下日志文件的创建:
- (NSString *)newLogFileName { NSString *appName = [self applicationName]; NSDateFormatter *dateFormatter = [self logFileDateFormatter]; NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]]; return [NSString stringWithFormat:@"%@ %@.log" , appName, formattedDate]; } - (NSDateFormatter *)logFileDateFormatter { NSMutableDictionary *dictionary = [[NSThread currentThread] threadDictionary]; NSString *dateFormat = @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'" ; NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@" , dateFormat]; NSDateFormatter *dateFormatter = dictionary[key]; if (dateFormatter == nil ) { dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX" ]]; [dateFormatter setDateFormat:dateFormat]; [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0 ]]; dictionary[key] = dateFormatter; } return dateFormatter; } - (NSString *)createNewLogFile { NSString *fileName = [self newLogFileName]; NSString *logsDirectory = [self logsDirectory]; NSUInteger attempt = 1 ; do { NSString *actualFileName = fileName; if (attempt > 1 ) { NSString *extension = [actualFileName pathExtension]; actualFileName = [actualFileName stringByDeletingPathExtension]; actualFileName = [actualFileName stringByAppendingFormat:@" %lu" , (unsigned long )attempt]; if (extension.length) { actualFileName = [actualFileName stringByAppendingPathExtension:extension]; } } NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName]; if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSDictionary *attributes = nil ; [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes]; [self deleteOldLogFiles]; return filePath; } else { attempt++; } } while (YES ); }
从上面可以看出日志文件的格式为“应用名 yyyy-MM-dd–HH-mm-ss-SSS.log”存储在缓存目录下的”Log”目录,再createNewLogFile主要处理同名的情况下会追加一个数字用于避免文件名同名,比如“应用名 yyyy-MM-dd–HH-mm-ss-SSS 1.log”,“应用名 yyyy-MM-dd–HH-mm-ss-SSS 2.log”等。创建一个新的文件之后还会调用deleteOldLogFiles对当前日志文件目录下对文件进行一遍检查。
我们在介绍DDFileLogger之前对DDLogFileManagerDefault 做个总结,DDLogFileManagerDefault总地来说就是负责日志文件的创建以及根据我们为日志文件设置的存储空间,以及日志文件数量进行管控,一旦超过空间大小,或者日志文件数量大于我们设定的最大数量都会删除掉最旧的那个。DDLogFileManager整体就是为了DDFileLogger服务的所以我们接下来看下DDFileLogger是怎么通过它来管理日志文件的。
初始化:
- (instancetype )init { DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; return [self initWithLogFileManager:defaultLogFileManager]; } - (instancetype )initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager { if ((self = [super init])) { _maximumFileSize = kDDDefaultLogMaxFileSize; _rollingFrequency = kDDDefaultLogRollingFrequency; _automaticallyAppendNewlineForCustomFormatters = YES ; logFileManager = aLogFileManager; self .logFormatter = [DDLogFileFormatterDefault new]; } return self ; }
在初始化方法中主要初始化了logFileManager,logFormatter以及_rollingFrequency,紧接着我们从最核心的日志输出入手看下整个DDFileLogger的细节。
- (void )logMessage:(DDLogMessage *)logMessage { NSString *message = logMessage->_message; BOOL isFormatted = NO ; if (_logFormatter) { message = [_logFormatter formatLogMessage:logMessage]; isFormatted = message != logMessage->_message; } if (message) { if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) && (![message hasSuffix:@"\n" ])) { message = [message stringByAppendingString:@"\n" ]; } NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding ]; @try { [self willLogMessage]; [[self currentLogFileHandle] writeData:logData]; [self didLogMessage]; } @catch (NSException *exception) { exception_count++; if (exception_count <= 10 ) { if (exception_count == 10 ) { NSLogError (@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them." ); } } } } }
我们先看下默认的文件日志格式化器是怎么对消息进行格式化的:
- (NSString *)formatLogMessage: (DDLogMessage *)logMessage { NSString *dateAndTime = [_dateFormatter stringFromDate: (logMessage->_timestamp)]; // 时间格式为yyyy/MM /dd HH :mm :ss :SSS return [NSString stringWithFormat: @"%@ %@" , dateAndTime, logMessage->_message]; }
formatLogMessage比较简单,就是将日志时间和日志内容进行拼接后返回。没有太大的特别处理,然后根据_automaticallyAppendNewlineForCustomFormatters的值判断是否需要在格式化后的消息尾部添加\n,在日志将要输出以及输出之后会分别调用willLogMessage,以及didLogMessage告诉外界将要开始写文件了,以及日志已经写到日志文件了。
[[self currentLogFileHandle] writeData:logData];
- (NSFileHandle *)currentLogFileHandle { if (_currentLogFileHandle == nil ) { NSString *logFilePath = [[self currentLogFileInfo] filePath]; _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; [_currentLogFileHandle seekToEndOfFile]; if (_currentLogFileHandle) { [self scheduleTimerToRollLogFileDueToAge]; _currentLogFileVnode = dispatch_source_create( DISPATCH_SOURCE_TYPE_VNODE, [_currentLogFileHandle fileDescriptor], DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, self .loggerQueue ); dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool { [self rollLogFileNow]; } }); dispatch_resume(_currentLogFileVnode); } } return _currentLogFileHandle; } - (DDLogFileInfo *)currentLogFileInfo { if (_currentLogFileInfo == nil ) { NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos]; if ([sortedLogFileInfos count] > 0 ) { DDLogFileInfo *mostRecentLogFileInfo = sortedLogFileInfos[0 ]; BOOL shouldArchiveMostRecent = NO ; if (mostRecentLogFileInfo.isArchived) { shouldArchiveMostRecent = NO ; } else if ([self shouldArchiveRecentLogFileInfo:mostRecxentLogFileInfo]) { shouldArchiveMostRecent = YES ; } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) { shouldArchiveMostRecent = YES ; } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) { shouldArchiveMostRecent = YES ; } if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) { _currentLogFileInfo = mostRecentLogFileInfo; } else { if (shouldArchiveMostRecent) { mostRecentLogFileInfo.isArchived = YES ; if ([logFileManager respondsToSelector:@selector (didArchiveLogFile:)]) { [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)]; } } } } if (_currentLogFileInfo == nil ) { NSString *currentLogFilePath = [logFileManager createNewLogFile]; _currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath]; } } return _currentLogFileInfo; }
整个文件的创建过程都放在currentLogFileHandle中,首先会从日志文件夹中取出最新一个文件,然后判断它是否需要achive了,判断的标准先看它是否已经achive了,然后再调用外部的shouldArchiveRecentLogFileInfo来判断是否需要achive,最后再看日志文件的大小以及age是否超过限制,如果超过限制则achive。achive大家理解吗?比较难解释,就是某个日志文件不用了就要将其归档起来,上传日志的时候就上传这些归档后的日志。如果当前第一个文件不是achive文件,那么就将它赋给_currentLogFileInfo,将文件光标指向文件最后,继续往这个日志文件下写日志。这里还有一个比较重要的方法scheduleTimerToRollLogFileDueToAge ,它用于定时扫描查看文件是否age过期了,每个日志文件都只能保留指定的时间,默认的情况下是24小时。我们来看下这个方法:
- (void )scheduleTimerToRollLogFileDueToAge { if (_rollingTimer) { dispatch_source_cancel(_rollingTimer); _rollingTimer = NULL ; } if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0 ) { return ; } NSDate *logFileCreationDate = [_currentLogFileInfo creationDate]; NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate]; ti += _rollingFrequency; NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti]; _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0 , 0 , self .loggerQueue); dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool { [self maybeRollLogFileDueToAge]; } }); uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * (NSTimeInterval ) NSEC_PER_SEC ); dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay); dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1 ull * NSEC_PER_SEC ); dispatch_resume(_rollingTimer); }
- (void)maybeRollLogFileDueToAge { if (_rollingFrequency > 0.0 && _currentLogFileInfo .age >= _rollingFrequency ) { [self rollLogFileNow]; } else { [self scheduleTimerToRollLogFileDueToAge]; } } - (void)rollLogFileNow { if (_currentLogFileHandle == nil ) { return; } [_currentLogFileHandle synchronizeFile]; [_currentLogFileHandle closeFile]; _currentLogFileHandle = nil ; _currentLogFileInfo .isArchived = YES; if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) { [logFileManager didRollAndArchiveLogFile:(_currentLogFileInfo .filePath)]; } _currentLogFileInfo = nil ; if (_currentLogFileVnode ) { dispatch_source_cancel(_currentLogFileVnode ); _currentLogFileVnode = NULL; } if (_rollingTimer ) { dispatch_source_cancel(_rollingTimer ); _rollingTimer = NULL; } }
scheduleTimerToRollLogFileDueToAge会查看文件的创建时间,然后在它的基础上加上_rollingFrequency,将它作为定时器的值定时,在这之后检查当前日志文件是否过期了,如果没有过期则再次进入下一轮。rollLogFileNow就是用于将文件归档。它会关闭文件句柄,_currentLogFileInfo.isArchived其实是这里最为关键的代码。
- (void )setIsArchived:(BOOL )flag { if (flag) { [self addExtendedAttributeWithName:kDDXAttrArchivedName]; } else { [self removeExtendedAttributeWithName:kDDXAttrArchivedName]; } } - (void )addExtensionAttributeWithName:(NSString *)attrName { if ([attrName length] == 0 ) { return ; } NSArray *components = [[self fileName] componentsSeparatedByString:@"." ]; NSUInteger count = [components count]; NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1 ; NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; if (count > 0 ) { [newFileName appendString:components.firstObject]; } NSString *lastExt = @"" ; NSUInteger i; for (i = 1 ; i < count; i++) { NSString *attr = components[i]; if ([attr length] == 0 ) { continue ; } if ([attrName isEqualToString:attr]) { return ; } if ([lastExt length] > 0 ) { [newFileName appendFormat:@".%@" , lastExt]; } lastExt = attr; } [newFileName appendFormat:@".%@" , attrName]; if ([lastExt length] > 0 ) { [newFileName appendFormat:@".%@" , lastExt]; } [self renameFile:newFileName]; }
它就是在文件名的最末尾添加一个archived标记,下一次再次调用currentLogFileInfo的时候就会略过这个文件。
到此为止已经带大家过了一遍CocoaLumberjack的核心源码了,至于DDASLLogger,DDOSLogger,以及DDTTYLogger这里就不再继续展开了,代码都很简单,但是这里还是要简单提下:
DDASLLogger DDASLLogger会在初始化后通过asl_open来建立连接,通过asl_new创建一个新的消息,然后通过asl_set来设置一系列的属性,最后通过asl_send将日志发送出去。
DDOSLogger DDOSLogger则是通过os_log_error,os_log_info,os_log_debug将日志发送出去的。
DDTTYLogger DDTTYLogger是通过writev(STDERR_FILENO,…,….);将日志发送到StdError终端的。