开源库信息

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>

// 用于格式化输出Log的格式
@property (nonatomic, strong) id <DDLogFormatter> logFormatter;

//每个logger相对于其他的logger都是并行执行的,所以每个logger都有一个专门的分发队列。
@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;

//如果logger实现方法没有提供它专有的队列,那么我们会自动为它创建一个,队列的名称将会通过这个方法获取
@property (nonatomic, readonly) NSString *loggerName;

//由于log是异步的,并且添加和删除logger也是异步的,在logger被添加到logger系统之前或者被移除之后都不会收到消息,logger可以通过这些方法做一些初始化,善后操作等
- (void)didAddLogger;
- (void)didAddLoggerInQueue:(dispatch_queue_t)queue;
- (void)willRemoveLogger;

// 执行Log输出的方法
- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));

//一些logger可能为了优化性能会使用缓存,这些缓存必须在退出应用之前进行flush操作将数据写到对应的目标。
- (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;
//当前logger使用global queue作为logger输出队列
@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue) BOOL onGlobalLoggingQueue;
//当前logger使用内部设计的queue作为logger输出队列
@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue;

@end

2.2 Loggers

DDASLLogger:ASL是Apple System Log的缩写,它用于发送日志到苹果的日志系统,以便它们显示在Console.app上。
DDTTYLogger:用于将日志发送到Xcode控制台。
DDFileLogger:用于将日志写入到日志文件下。
DDOSLogger:用于将日志写入到os_log
DDAbstractDatabaseLogger:是数据库日志的基础类,用于方便我们进行扩展。
一般我们如果只需要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          //根据上面分析,默认这里为YES
message:(NSString *)message //这是要输出的log消息
level:(DDLogLevel)level //消息等级
flag:(DDLogFlag)flag //消息flag
context:(NSInteger)context //附加context 这里为0
file:(const char *)file //当前文件
function:(const char *)function //方法名
line:(NSUInteger)line //输出log的行
tag:(id)tag { //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) {
// 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);
} 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; // 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);
});
}

每次初始化的时候都会调用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 {
// Add to loggers array.
// Need to create loggerQueue if loggerNode doesn't provide one.

//判断是否已经存在了
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;

//判断当前logger是否有自己的队列
if ([logger respondsToSelector:@selector(loggerQueue)]) {
loggerQueue = [logger loggerQueue];
}
//如果没有自己的队列,则使用logger提供的loggerName方法返回的名字,创建队列
if (loggerQueue == nil) {
const char *loggerQueueName = NULL;
if ([logger respondsToSelector:@selector(loggerName)]) {
loggerQueueName = [[logger loggerName] UTF8String];
}
loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
}

//将logger,logger队列,等级封装到DDLoggerNode后添加到_loggers
DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue level:level];
[self._loggers addObject:loggerNode];

//通知外部当前logger已经被添加了
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;
//是否对传进来的日志文件进行打包成achive文件
- (BOOL)shouldArchiveRecentLogFileInfo:(DDLogFileInfo *)recentLogFileInfo;
//日志文件的最大尺寸,超过这个大小将会被回滚,如果maximumFileSize设置为0则只有rollingFrequency会影响到文件的回滚
@property (readwrite, assign) unsigned long long maximumFileSize;
// 回滚的频率,超过这个时间就会被回滚,maximumFileSize设置为0或者任何负数则只有maximumFileSize会影响到文件的回滚
@property (readwrite, assign) NSTimeInterval rollingFrequency;
// 在每次启动的时候会自动创建新的日志文件,不会重用旧的
@property (readwrite, assign, atomic) BOOL doNotReuseLogFiles;
// 日志文件管理类
@property (strong, nonatomic, readonly) id <DDLogFileManager> logFileManager;
// 会在每条日志的最后添加\n
@property (nonatomic, readwrite, assign) BOOL automaticallyAppendNewlineForCustomFormatters;
// 强制roll当前日志
- (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;      // 1 MB
NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours
NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files
unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB
@protocol DDLogFileManager <NSObject>
@required

// Public properties

// 磁盘上能够存储的最多archived log文件数量,
// 比如我们将这个值设置为3,那么LogFileManager只会保存当前日志文件在内的3个日志文件,如果要禁止这个功能可以将这个值设置为0
@property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles;

// 日志文件所能占用的空间大小
@property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota;

// Public methods
// 日志文件目录
@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;

// Private methods (only to be used by DDFileLogger)
// 创建新的Log文件
- (NSString *)createNewLogFile;

@optional

// Notifications from DDFileLogger
// 在日志文件被archieved的时候被调用
- (void)didArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didArchiveLogFile(atPath:));

// 在日志文件被roll的时候被调用
- (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 {

//将缓存目录下的所有日志进行排序形成sortedLogFileInfos数组
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) {
//如果_automaticallyAppendNewlineForCustomFormatters = YES,就会在格式化日志的后面追加\n
if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) &&
(![message hasSuffix:@"\n"])) {
message = [message stringByAppendingString:@"\n"];
}
//将数据转换为NSData
NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding];
@try {
//通知外部将要写日志了,这样可以在开始写日志前做一些处理
[self willLogMessage];
//写入日志
[[self currentLogFileHandle] writeData:logData];
//通知外部日志写入成功了
[self didLogMessage];
} @catch (NSException *exception) {
//如果出问题则重试10次
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];
//创建NSFileHandle
_currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
//将文件光标移到文件的最后
[_currentLogFileHandle seekToEndOfFile];
if (_currentLogFileHandle) {
//开始定时roll
[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;
//如果已经archived则shouldArchiveMostRecent = NO
if (mostRecentLogFileInfo.isArchived) {
shouldArchiveMostRecent = NO;
//通过外部方法拦截判断当前日志文件是否需要archive
} else if ([self shouldArchiveRecentLogFileInfo:mostRecxentLogFileInfo]) {
shouldArchiveMostRecent = YES;
} else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
//如果当前文件尺寸大于_maximumFileSize则需要archive
shouldArchiveMostRecent = YES;
} else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
//如果当前文件age超过了_rollingFrequency则需要archive
shouldArchiveMostRecent = YES;
}
//.......
// 如果是未achive的日志文件表示,当前可用的,将其赋给_currentLogFileInfo
if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) {
_currentLogFileInfo = mostRecentLogFileInfo;
} else {
//如果需要achive当前文件则将当前文件设置为achive并通知外部
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, 1ull * 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 {
// This method is only used on the iPhone simulator, where normal extended attributes are broken.
// See full explanation in the header file.
if ([attrName length] == 0) {
return;
}
// Example:
// attrName = "archived"
//
// "mylog.txt" -> "mylog.archived.txt"
// "mylog" -> "mylog.archived"
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终端的。

Contents
  1. 1. 开源库信息
  2. 2. 源码解析