CocoaLumberjack is a fast & simple, yet powerful & flexible logging framework for Mac and iOS.
先扯一下 lumberjack 这个单词,对应的就是它的 logo,一位伐木工,好想知道作者对用意啊,是基础建设的意思吗?
写这篇文章的理由并非心血来潮,而是最近在使用过程中偶然发现,它居然有这么多隐藏功能,尽管项目里引入也有好多年了。接着又看了一下官方提供的 demos, 简直是惊呆了(也太丰富了吧,强烈建议各位看看官方 Demo)。最后,因为国内基本都是关于它的使用介绍,本文希望能从代码的角度来看看它的一些设计和。最后会介绍一下它所支持的扩展。
作为历史悠久的 library,它的 document 还是非常详细的,主要分三个级别:
• Beginner 入门级:使用说明、自定义日志格式、性能测试、支持彩色输出等;
• Intermediate 进阶:lumberjack 内部概述,如何定制 custom logging context、custrom logger、custom log levels 等;
• Advanced:高阶:动态修改 log levels、log 文件管理(压缩、上传。
照例,我们先预览一下类图,有个大概的印象。
在梳理完脑图才发现官方其实提供了完整的 UML 图。不过既然整理了脑图,那我把它贴在文末。
UML 上直观感受就是 class 并不多,但是功能确实十分完善,我们一点点来看看。
本文默认你是经历过新手村的,如果对 Lumberjack 的 API 完全不熟悉,请挪步:getting start。
核心文件 DDLog.h 中有声明了最重要的两个协议 DDLoger 和 DDLogFormatter,而 DDLog class 可以看作是一个 manager 的存在,它管理着所有注册在案的 loogers 和 formatters。这三个对于正常项目来说已经完全够用了。我们就从 protocol 着手,最后来说这个 DDLog。
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 会存有哪些可用信息呢?
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();
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。
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 会被自动调用。当然,作为开发者我们可以在适当的情况下手动触发刷新,正常是不需要手动触发的。
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 被移除时的通知。
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。因此,作为单例是必须的。我们先来看看它初始化都准备了什么东西。
@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 的操作主要是添加和删除。
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 相关方法是 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 { ... }
• 多核处理器,代码如下:
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 中,打包成一个 "事务",以保证每次的 ltlog: 都是顺序执行的。而每个 logger 本身都分配了独立的 loggerQueue,通过这种组合,即保证了 logger 的并发调用,又能满足 queueSize 的限制。
使用 dispatch_group_wait 还有一个目的,就是确保那些执行效果慢的 logger 也能按顺序完成调用,避免队列任务过多时,这些 logger 没能及时完成导致大量的 padding log message 没有被及时处理。
• 对单核处理就比较简单了,就是第二步不同。不存在 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 的调度,主要做了以下工作:
• 管理 logger 的生命周期,并对其添加、删除操作进行相应通知;
• 生成 logMessage 并在线程安全的情况下,将其分配到对应的 logger 以加工 message。
• 在程序结束后,及时通知 logger 清理 pending 状态的 message。
现在我们来聊聊 logger。DDLog 给我们提供了一个 logger 基类 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 :
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,这里默认返回的 loggerName 是 NSStringFromClass([self class]); 。
同时,以 self 的地址作为 flag 关联到 loggerQueue,并用于判断 onInternalLoggerQueue 。
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 过程中却都是异步调用的。
• log message 最终是在单独的 loggerQueue 中执行的,是由 logger 各自持有的 queue;
• 在进入每个 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];
}
}
}
ASLLogger 是对 Apple System Log API 的封装,我们经常使用的 NSLog 会将其输出定向到两个地方:
• Apple System Log
• Standard error (telemetry)
不过 ASLLogger 在 macosx 10.12 iOS 10.0 已经被废弃了,取而代之的是 DDOSLoger。ASLLogger 背后使用的 API 是 ,它也提供了几种 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 是每个 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);
}
苹果的新一代 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 有机会在展开。
首先,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 详细点此。
同样是过滤 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;
}
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。
TTYLogger 支持为每一种 logFlag 配置不同的颜色,然后将 color 与 flag 封装进 DDTTYLoggerColorProfile 类中,存储在 _colorProfilesDict 中。logMessage 主要分三步:
• 通过 logMessage->_tag 取出 colorProfile;
• 将 log message 转为 c string;
• 将 color 写入 iovec v[iovec_len],最终调用
• writev(STDERR_FILENO, v, iovec_len); 输出。
以上三种 logger 属于基本的终端输出,可用于替代 NSLog。限于篇幅的原因,还有 DDFileLogger、DDAbstractDatabaseLogger 以及各种扩展,如 WebSocketLogger 等,未在本篇出现。同时还有一整节的 Formatters 均放下一篇中。
本篇,通过 DDLog 类对 GCD 的使用,看到了 lumberjack 的作者充分利用了 GCD 的特性来达到安全高效的异步 logging。整个过程中并未使用锁来解决线程安全,算是对 GCD 的很好实践了。该作者还出品了 CocoaAsyncSocket 、XMPPFramework、CocoaHTTPServer 等知名的库。之后可以慢慢细品。
最后,贴一张整理的脑图,比较简单,不喜勿喷。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。