文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. CocoaLumberjack的使用
  4. 4. CocoaLumberjack源码分析
  5. 5. 桥接模式
  6. 6. 扩展CocoaLumberjack
  7. 7. 总结

文档更新说明

  • 最后更新 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
// 自己需要定义这个变量, 确定实际上需要的日志级别.
static const DDLogLevel ddLogLevel = DDLogLevelDebug;
// 先创建一个文件日志, 定义好相关配置信息
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
fileLogger.logFormatter = [[DDDispatchQueueLogFormatter alloc] init];
fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
// 添加一个logger
[DDLog addLogger:fileLogger];
// 可以同时加入多个, 比如再加入一个控制台logger, 这样文件和控制台都会有日志信息输出
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
[DDLog
log:NO
level:ddLogLevel
flag:DDLogFlagDebug
context:0
file:__FILE__
function:__FUNCTION__
line:__LINE__
tag:nil
format:@"Debug"
];

上面这个类方法, 一层一层调用下去, 直到下面代码:

1
2
3
4
// DDLog.m
// 将参数封装成
[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
// DDLog.m
// 程序开始调用下面方法, 进入初始化完成后的阶段
- (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
// DDLog.m
- (void)lt_log:(DDLogMessage *)logMessage {
///省略其他代码
for (DDLoggerNode *loggerNode in self._loggers) {
// skip the loggers that shouldn't write this message based on the log level
// 从当前的DDLog实例里取出所有存入的logger节点, 里面就有我传入的logger对象
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
// DDFileLogger.m
// 这里分析的Logger是沙盒日志
- (void)logMessage:(DDLogMessage *)logMessage {
/// 省略其他代码
// 这里可以看到框架开始使用我传入的Formatter对象了
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
//
// CCLogFormatter.h
// OCSimpleView
//
// 自定义格式化信息, 用于CocoaLumberjack框架
// Created by Cocos on 2019/7/25.
// Copyright © 2019 Cocos. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
NS_ASSUME_NONNULL_BEGIN
@interface CCLogFormatter : NSObject <DDLogFormatter>
/**
* Default initializer
*/
- (instancetype)init;
/**
* Designated initializer, requires a date formatter
*/
- (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
//
// CCLogFormatter.m
// OCSimpleView
//
// Created by Cocos on 2019/7/25.
// Copyright © 2019 Cocos. All rights reserved.
//
#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]; // 10.4+ style
[_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多种, 实在是太多了, 学不完, 但是保持一颗年轻的心, 活到老学到老, 我觉得是最重要的 : )

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. CocoaLumberjack的使用
  4. 4. CocoaLumberjack源码分析
  5. 5. 桥接模式
  6. 6. 扩展CocoaLumberjack
  7. 7. 总结