文档更新说明
- 最后更新 2019年07月25日
- 首次更新 2019年07月25日
前言
在编程领域里, 听的多做得少的就是设计模式. 很多程序员都听说过设计模式, 但是却很少自己手动实现一些真正意义上的设计模式, 这几天刚好在复习设计模式, 然后今天又看了iOS上流行的以灵活,扩展性高著称的开源日志框架CocoaLumberjack的源码, 有感而发, 下面我想好好谈谈心得, 一步一步揭开这个高度灵活的框架是如何设计的.
CocoaLumberjack的使用
先来简单看一下CocoaLumberjack的使用. CocoaLumberjack为用户提供了几种日志模式, 有控制台, 系统日志, 沙盒日志, 此外还有日志不可或缺的格式化功能Formatter. 下面例子以最复杂的沙盒日志为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static const DDLogLevel ddLogLevel = DDLogLevelDebug;
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; fileLogger.logFormatter = [[DDDispatchQueueLogFormatter alloc] init]; fileLogger.rollingFrequency = 60 * 60 * 24; fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
[DDLog addLogger:fileLogger];
DDTTYLogger *log2 = [DDTTYLogger sharedInstance]; log2.logFormatter = [[CCLogFormatter alloc] init]; [DDLog addLogger:log2];
DDLogDebug(@"Debug");
|
上面简单的代码,就完成了日志格式的定制, 日志的输出定制, 日志的沙盒文件配置等功能, 灵活设置. 此外还演示了用户自行扩展的格式化类, 用于自行格式化输出内容, 像这样灵活的设计, 确实很值得赞赏, 下面一起来看看源码, 这是如何实现.
CocoaLumberjack源码分析
展开宏之后得到下面的真实执行入口, 下面主要分析沙盒日志源码
1 2 3 4 5 6 7 8 9 10 11 12
| [DDLog log:NO level:ddLogLevel flag:DDLogFlagDebug context:0 file:__FILE__ function:__FUNCTION__ line:__LINE__ tag:nil format:@"Debug" ];
|
上面这个类方法, 一层一层调用下去, 直到下面代码:
1 2 3 4
|
[self.sharedInstance log:asynchronous message:message level:level flag:flag context:context file:file function:function line:line tag:tag];
|
这是遇到的第一个设计模式, 也是最常见的, 单例, 没什么好说的, 接着往下执行:
1 2 3 4 5 6 7 8
|
- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag { [self lt_log:logMessage]; }
|
程序执行了lt_log方法, 把消息传进去, 接着往下执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
- (void)lt_log:(DDLogMessage *)logMessage { for (DDLoggerNode *loggerNode in self._loggers) { if (!(logMessage->_flag & loggerNode->_level)) { continue; } dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool { [loggerNode->_logger logMessage:logMessage]; } }); } }
|
上面代码, 注意需要注意的是 _logger的类型是id <DDLogger>
, 这就是我要讲的第二种编程模式, 针对抽象编程, 也就是代码只关心对象实现了那些接口, 只要实现了规定的接口, 下面就可以安全调用接口即可, 不关心对象具体的类型. 继续往下执行:
1 2 3 4 5 6 7 8 9 10 11 12
|
- (void)logMessage:(DDLogMessage *)logMessage { if (_logFormatter != nil) { message = [_logFormatter formatLogMessage:logMessage]; isFormatted = message != logMessage->_message; } [self lt_logData:[message dataUsingEncoding:NSUTF8StringEncoding]]; }
|
上面代码, 需要注意的地方是_logFormatter这个变量的类型是id <DDLogFormatter>
, 和上一步提到的原理一样, 这里其实也是第三种设计模式: 桥接模式(有点像策略模式, 后面解释).
框架最后执行到了lt_logData:
方法后, 才开始真正写日志.通过上面的分析, 可以知道我传入的Logger和Formatter是如何起作用的, 也可以看出来框架底层采用接口编程这个套路, 兼容了传入的任何适合的Logger和Formatter, 让用户可以灵活扩展自己的日志需求.
桥接模式
写到这里我其实也不管100%告诉你CocoaLumberjack主要利用桥接模式实现可扩展高度灵活的功能, 因为有好几种设计模式都是类似这样的, 我也不是写论文似的分析, 姑且就叫桥接模式吧.
实际上桥接模式和策略模式是有点像, 策略模式更多的应该是表示行为算法, 本身实现了某个策略的类被聚合进主类中时, 更多的是提供简单的无状态算法功能. 而CocoaLumberjack框架可以看到, 像Formatter和Logger这些类本身是包含比较复杂的逻辑和内部对象属性, 所以我觉得更适合把他们叫做桥接模式.
说了这么多, 那什么是桥接模式呢? 具体定义可以自行谷歌搜索一下, 他长得就是文章上面分析的那样, 这里我引用一下菜鸟教程里面的关于桥接模式的表述, 我觉得很合适.
学习了桥接模式的设计方法之后, 再看CocoaLumberjack框架的源码, 就会发现, 好像是这么回事, 很精彩. 看懂了源码之后, 下面就来自行定制一下输出格式, 这整个过程几乎不需要上网查任何资料, 自给自足即可.
扩展CocoaLumberjack
通过上面分析可知, 只要我的类实现了DDLogFormatter
协议, 就可以作为一个合法的Formatter对象做为DDLog的配置. 下面看看扩展的Formatter源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
#import <Foundation/Foundation.h> #import <CocoaLumberjack/CocoaLumberjack.h>
NS_ASSUME_NONNULL_BEGIN
@interface CCLogFormatter : NSObject <DDLogFormatter>
- (instancetype)init;
- (instancetype)initWithDateFormatter:(NSDateFormatter * __nullable)dateFormatter NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
#import "CCLogFormatter.h"
@interface CCLogFormatter () { NSDateFormatter *_dateFormatter; }
@end
@implementation CCLogFormatter
- (instancetype)init { return [self initWithDateFormatter:nil]; }
- (instancetype)initWithDateFormatter:(NSDateFormatter * __nullable)aDateFormatter { if ((self = [super init])) { if (aDateFormatter) { _dateFormatter = aDateFormatter; } else { _dateFormatter = [[NSDateFormatter alloc] init]; [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"]; } } return self; }
- (NSString * _Nullable)formatLogMessage:(nonnull DDLogMessage *)logMessage { NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)]; return [[NSString alloc] initWithFormat:@"[%@]\n%@:\n[%@]%@", logMessage.file, logMessage.function, dateAndTime, logMessage.message]; }
@end
|
其实还是挺简单的, 我实现了formatLogMessage:这个方法, 其他的代码直接从DDLogFileFormatterDefault
类抄过来, 当然这里直接让CCLogFormatter
继承DDLogFileFormatterDefault
的话这里初始化代码就可以省略了. 在CCLogFormatter
里我让这个格式从原来:
2019/07/25 14:33:36:239 Debug
换成了如下格式:
[~/Xcode/Study/OCSimpleView/OCSimpleView/ViewController.m]
-[ViewController loggerFunc]:
[2019/07/25 14:58:07:358]Debug
这样扩展就结束了. 其他更多复杂的功能, 百变不离其宗, 都是这个套路.
总结
这篇文章主要是通过对热门框架CocoaLumberjack源码的分析, 找出其中的设计模式, 进而了解设计模式的重要性, 学会灵活扩展框架的实现方法之一. 还有很多设计模式, 传说一共有30多种, 实在是太多了, 学不完, 但是保持一颗年轻的心, 活到老学到老, 我觉得是最重要的 : )