导语:
DDLog,即CocoaLumberjack是iOS开发用的最多的日志框架,出自大神Robbie Hanson之手(还有诸多知名开源框架如 XMPPFramework、 CocoaAsyncSocket,都是即时通信领域很基础应用很多的框架)。了解DDLog的源码将有助于我们更好的输出代码中的日志信息,便于定位问题,也能对我们在书写自己的日志框架或者其他模块时有所启发。
此系列文章将分为以下几篇:
- DDLog源码解析一:框架结构
- DDLog源码解析二:设计初衷
- DDLog源码解析三:FileLogger
引言:为什么需要DDLog?
我们在iOS入门阶段最早能通过代码得到的反馈,可能就是打印日志,那时我们通常会遇到第一个朋友:NSLog,这是iOS系统的默认打印日志的方式。当我们在初级开发阶段NSLog已经足够好,帮我们留下必要信息便于定位问题。
但随着App复杂度的增加,调试起来变得麻烦,NSLog的性能也渐渐成为了瓶颈,我们也开始有了一些个性的需求,比如想把某一类日志信息用红色显示在控制台,比如写在文件中的日志只写我们认为很关键模块的部分...... 这时候NSLog已经无法满足我们的需求,我们会发现除了自己造轮子,就只能找轮子了,幸好有DDLog。
需求:DDLog能干什么?
功能上的需求可能包括:
- 把日志写到Xcode台上
- 把日志写到文件里
- 把日志写到iOS系统日志中
- 把日志规定多个级别,我们的日志可以归类到不同级别;
- 根据日志级别,我们可以只输出某个级别的日志;
- 根据日志级别,我们可以对某些级别日志加颜色显示;
- 对某个类设定日志级别;
- ......
但是,具备这些功能后,我们可能就要关注三个指标:
准确!
快速!
安全!
准确是最基本的,我们要保证日志如实的记录我们记录的东西,内容和日志顺序、时间等都是正确的; 快速也是比较重要的点,试想我们的高清视频通话的功能在通话时,如果实时打印出很多信息并且写到文件中,如果性能不过关,就可能会影响视频通话的效果;安全范围很宽泛,除了记录内容的线程安全外,最直接的就是不对app造成过大侵犯,比如写到文件中内容过多,将导致app大小剧增,对于手机容量有限的用户将造成很大体验上的影响。
而DDLog的设计上考虑了这几点,所以我们有必要解析一下DDLog在哪些方面的设计来满足这些需求:
正文
想知道DDLog如何此般强大,我们首先对DDLog的框架进行解析,先看下官方的框架示意图(已经与代码部分不符合,但不影响理解):
本文将主要对上图中几个重要的类(DDLog、DDLogger、DDAbstractLogger、DDTTYLogger、DDOSLogger、DDFileLogger、DDASLLogger)及其之间的关系进行分析,DDLog主要是通过四种logger分别提供给开发者四个方面日志输出的能力,对应于上面的顺序依次是
DDTTYLogger:写到Xcode控制台、
DDOSLogger:写到iOS10之后的系统日志、
DDFileLogger:写到文件中、
DDASLLogger:写到iOS10之前的系统日志,
而DDLog类是对这四种logger进行管理,统一处理日志的输出的问题。其余类包含fomatter(自定义输出日志的格式和内容)之类的处理等,本文不做解析。
下图是我整理后的图:
DDLogger
这个协议主要定义了logger一些通用的行为:
@protocol DDLogger <NSObject>
- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));
@property (nonatomic, strong) 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 (nonatomic, readonly) NSString *loggerName;
@end
DDAbstractLogger
作为遵守了DDLogger协议的基类,主要是通过一些属性和方法,描述子类一些通用的行为和能力:
@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方法中定义了子类都要使用的串行队列:
_loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
// loggerQueueName由各个子类名字构成
void *key = (__bridge void *)self;
void *nonNullValue = (__bridge void *)self;
dispatch_queue_set_specific(_loggerQueue, key, nonNullValue, NULL);
需要注意的是,子类init再调用这个基类的init方法时,实际self是相应的子类,这样就会根据不同子类生成不同的_loggerQueue,并且通过dispatch_get_specific和dispatch_queue_set_specific一对好基友来标识识别每个队列。
- (NSString *)loggerName {
return NSStringFromClass([self class]);
}
- (BOOL)isOnGlobalLoggingQueue {
return (dispatch_get_specific(GlobalLoggingQueueIdentityKey) != NULL);
}
- (BOOL)isOnInternalLoggerQueue {
void *key = (__bridge void *)self;
return (dispatch_get_specific(key) != NULL);
}
DDLog
真正的BOSS,管理各个logger的add和remove,并暴露各种记录日志的log方法,这一步将在[下一节](DDLog源码解析二:线程)详细解析,主要是线程的保护机制,这里的线程保护机制包括并不限于:保证log语句按顺序记录下来,保证每个logger的添加、移除和level的改变等机制都能立刻再后面的log语句中生效,如何保证各个logger中最终记录的下来的日志是相同的(不会发生某一个logger的日志比其他的多几条)......
注意,由于initialize是在类或者其子类的第一个方法被调用前调用,并且只会调用一次,在DDLog的类、子类或实例中可能用到DDLog中定义的这些资源,这里DDLog将相关公用的资源申请放在 类方法 +(void)initialize中,保证DDLog在第一次使用时就已经申请好公用资源。
+ (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);
});
}
这里申请的_queueSemaphore、_loggingQueue、_loggingGroup都是下一节将重点分析的部分。
DDFileLogger
DDFileLogger继承自DDAbstractLogger,在实例化时将调用DDAbstractLogger的init方法,从而得到自己的_loggerQueue,并实现了自己的logMessage方法和其他文件处理相关方法,第三节将具体介绍。
@interface DDFileLogger : DDAbstractLogger <DDLogger> {
DDLogFileInfo *_currentLogFileInfo;
}
DDASLLogger
DDASLLogger继承自DDAbstractLogger,主要功能是将日志写到ASL中,代码逻辑简单,但需要了解ASL相关api才能了解清楚,本文不做解析。
DDOSLogger
DDOSLogger继承自DDAbstractLogger,主要功能是将日志写到os_log中,代码逻辑简单,但需要了解os_log相关api才能了解清楚,本文不做解析。