前言
CocoaLumberjack是适用于Mac和iOS的快速,简单,功能强大且灵活的日志记录框架。
最近想要升级项目中的Log库,比较了一番之后,发现CocoaLumberjack这个库可自定义性特别强,能够满足我们的所有要求,而且使用人数很多,可维护性强,性能也不错。于是最终选择了这个库进行集成。集成这个库的过程也花了一段时间,下面会用一步一步介绍这个库的使用。最后会将这些功能集成到一个文件里面,方便使用。(话说好想吐槽下这个库的名字。。。)
本文Demo
基本使用
使用cocoapod安装库:pod 'CocoaLumberjack','~> 3.6.1'
由于Log库在所有的文件中都可能用到,故最好是在项目中的全局头文件PCH文件中导入Log库的头文件。另外,这个库有个挺特别的地方,需要我们自己定义一个ddLogLevel变量,用来定义当前日志的输出等级。
//项目的pch文件中
#import
//定义Log等级
static const DDLogLevel ddLogLevel = DDLogLevelDebug;
如上所示,上面定义了ddLogLevel等级为DDLogLevelDebug。
然后我们在项目入口进行初始化,就可以正常使用日志库了。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//添加控制台logger
[DDLog addLogger:[DDOSLogger sharedInstance]];
//打印,跟NSLog用法一致
DDLogError(@"DDLogError");
DDLogWarn(@"DDLogWarn");
DDLogInfo(@"My name is = %@",@"谦言忘语");
DDLogDebug(@"I am %d years old",18);
DDLogVerbose(@"DDLogVerbose");
return YES;
}
只需要将我们熟悉的NSLog替换为CocoaLumberjack提供的DDLogInfo等就可以了。所以使用起来还是挺简单的,符合我们以往的使用习惯。
跟NSLog不一样的地方是,CocoaLumberjack提供了不同等级的Log输出,一共有5种等级,以下为5种等级的宏定义。
DDLogError(frmt, ...) //错误 当ddLogLevel = DDLogLevelError及以上等级时会输出
DDLogWarn(frmt, ...) //警告 当ddLogLevel = DDLogLevelWarning及以上等级时会输出
D
DDLogInfo(frmt, ...) //信息 当ddLogLevel = DDLogLevelInfo及以上等级时会输出
D
DDLogDebug(frmt, ...) //调试 当ddLogLevel = DDLogLevelDebug及以上等级时会输出
D
DDLogVerbose(frmt, ...) //详细 当ddLogLevel = DDLogLevelVerbose及以上等级时会输出
输出的等级由我们刚开始定义的ddLogLevel决定。比如我们上面定义了ddLogLevel = DDLogLevelDebug
,那么debug及以下等级的Log都能够输出,而使用DDLogVerbose
的Log就不会输出。
Logger
CocoaLumberjack内置了以下几种Logger。
DDFileLogger:此类提供了一个记录器,可将日志语句写入文件。
DDOSLogger:此类提供了Apple os_log工具的记录器。
DDASLLogger:此类为Apple系统日志工具提供了一个记录器。
DDTTYLogger:此类为终端输出或Xcode控制台输出提供了记录器。
有必要解释以下几种Logger的区别。
DDFileLogger
:很容易理解,是将log写入到文件中。
DDOSLogger
:在iOS10开始使用,在将Log输出到 控制台.app 和 Xcode控制台。跟NSLog的输出方式一致。当然,经过处理之后,性能会比直接使用NSLog要好。
DDASLLogger
:将日志写入到控制台.app中。在iOS10开始过时
DDTTYLogger
:将日志写入到Xcode控制台。
而我们常用给的NSLog会将日志写入到控制台.app和Xcode控制台。
所以,想要替换NSLog,官方推荐的做法是:
在iOS10及以上系统版本,使用DDOSLogger。
在iOS10以下版本,使用DDASLLogger+DDTTYLogger。
另外,如果你在系统 控制台.app 里面查看log的时候,看不到对应的log,那很可能是因为你没有把 控制台.app 的【包括简介信息]和【包括调试信息】勾选上。
日志等级修改
由于项目需要,我们需要动态修改日志的等级。
就是说我们需要在APP运行过程中修改日志等级。比如启动的时候,默认等级为DDLogLevelWarning
。当我们需要查看更详细日志的时候,就将日志等级改为DDLogLevelVerbose
。
怎么做呢?
之前做的时候在网上搜索了一圈,发现相关的资料并不算多,有些做到了,但是搞得很复杂。
其实这个功能实现起来不需要那么复杂。CocoaLumberjack是通过上面我们定义的ddLogLevel这个变量来控制当前输出的日志等级的。只要我们在APP运行的某个时刻修改ddLogLevel的值就可以了。
所以我们需要对ddLogLevel修改下,去掉const修饰
//定义Log等级,默认为DDLogLevelWarning
static DDLogLevel ddLogLevel = DDLogLevelDebug;
然后适当的地方修改ddLogLevel的值
//修改Log等级为DDLogLevelVerbose
ddLogLevel = DDLogLevelVerbose
这样就可以实现动态修改等级的需求了。
但是这种方案不能实现我们的要求,因为这种方案只能修改某一个文件的内的log等级,就是说我们AppDelegate.m里面修改了log等级为DDLogLevelVerbose,那么就只有在这个类里面生效,在ViewController.m是不生效的。
于是采用了另外一种方案。
更好的日志等级动态修改方案
首先我们新建一个类(名字可以为SQLogManager
),在SQLogManager.h
文件中声明一个ddLogLevel
外部变量,再然后在SQLogManager.m
文件里面定义ddLogLevel
的初始化值。
//定义外部变量
extern DDLogLevel ddLogLevel;
@interface SQLogManager : NSObject
@end
#import "SQLogManager.h"
DDLogLevel ddLogLevel = DDLogLevelDebug;
@implementation SQLogManager
@end
然后pch文件导入该头文件
#import
#import "SQLogManager.h"
这样在外面任何的地方修改了ddLogLevel
的值,就能够全局生效了。
修改输出格式
我们平常用的NSLog的输出格式是这样的
2020-05-03 16:31:07.418620+0800 CocoaLumberjackDemo[2943:118557] DDLogVerbose
DDOSLogger的输出默认是这样子的
2020-05-03 16:31:07.420348+0800 CocoaLumberjackDemo[2943:118557] DDOSLogger格式test
可以看到两个是基本一样的。
但是,很多时候我们需要修改这个东西,比如我的需求就是在我们的打印前面统一加上前缀[SQ],这样在系统的 控制台.app
里面查看log的时候一搜索就能够轻易定位到我们APP自己的输出,而不是系统或者其他的输出,排除干扰(话说系统的日志控制台的log真多,坑)。当然,更多的人想要在前面输出当前的Log所在的类名-方法名-行数等等,便于定位问题。
CocoaLumberjack提供了很好的修改输出格式的方式。
- 首先,我们需要新建一个继承NSObject的类(比如命名为
SQLogFormatter
) - 然后遵循
DDLogFormatter
协议 - 实现DDLogFormatter协议里面的
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
方法
在方法里面就可以定义你所想要的格式。例如:
@implementation SQLogFormatter
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
NSString *logLevel = nil;
switch (logMessage->_flag) {
case DDLogFlagError:
logLevel = @"Error";
break;
case DDLogFlagWarning:
logLevel = @"Warn";
break;
case DDLogFlagInfo:
logLevel = @"Info";
break;
case DDLogFlagDebug:
logLevel = @"Debug";
break;
default:
logLevel = @"Verbose";
break;
}
NSString *formatLog = [NSString stringWithFormat:@"[SQ]%@%@-%ld %@",logLevel,logMessage->_function,logMessage->_line,logMessage->_message];
return formatLog;
}
@end
DDLogMessage
这个对象里面有很多可以用到的成员变量,这些都是可以利用的。
@interface DDLogMessage : NSObject
{
// 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;
}
- 接着我们需要设置
DDOSLogger
的logFormatter
属性
将我们刚才创建的SQLogFormatter
设置进去,就可以修改输出格式了。
//添加控制台logger
DDOSLogger *osLogger = [DDOSLogger sharedInstance];
osLogger.logFormatter = [[SQLogFormatter alloc] init];
[DDLog addLogger:osLogger];
来个对比结果如下:
2020-05-03 17:21:19.567059+0800 CocoaLumberjackDemo[3453:137167] NSLog格式test
2020-05-03 17:21:19.570009+0800 CocoaLumberjackDemo[3453:137215] [SQ]Info-[AppDelegate application:didFinishLaunchingWithOptions:]-49 DDOSLogger格式test
值得一说的是,每次打印日志之前,都会访问这个方法,所以不要在这个方法里面做太多的逻辑,不然会影响性能。
log写入到文件
之前介绍过,DDFileLogger
可以将日志写入到文件。所以,添加一个DDFileLogger
就可以将日志记录到文件里面了。
我们来个简单的,创建一个DDFileLogger
对象,然后添加到DDLog
。
//添加文件写入logger
DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
[DDLog addLogger:fileLogger];
默认的log日志在沙盒的Library/Caches/Logs目录中,是一个以.log
为后缀的文本文件。如下图所示,下面那个就是文本文件的内容
log文件的属性自定义
根据项目需求,我们需要对log文件做很多的自定义设置。
默认情况下,log文件在多次启动的时候是会重用的,24小时内将log写入到同一个文件中,当文件大小超过1MB或者创建时间超过24小时,会新生成一个log文件,后面的log会写入到新的文件中。文件的个数超过5个的时候,会删除旧的文件。logs文件夹大小超过20M的时候,也会删除旧的文件以释放磁盘空间。
CocoaLumberjack本身提供了这些属性让我们自定义。直接代码解释:
DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
//重用log文件,不要每次启动都创建新的log文件(默认值是NO)
fileLogger.doNotReuseLogFiles = NO;
//log文件在24小时内有效,超过时间创建新log文件(默认值是24小时)
fileLogger.rollingFrequency = 60*60*24;
//log文件的最大3M(默认值1M)
fileLogger.maximumFileSize = 1024*1024*3;
//最多保存7个log文件(默认值是5)
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
//log文件夹最多保存10M(默认值是20M)
fileLogger.logFileManager.logFilesDiskQuota = 1014*1024*20;
//添加文件写入logger
[DDLog addLogger:fileLogger];
Log文件夹与文件名的获取
有些时候我们想要获取到log文件所在的路径,用来进行一些操作。比如将log文件读取出来查看,或者将log文件上传到服务器去。这些路径可以从fileLogger
对象里面能够获取到。
//logs文件夹路径
DDLogInfo(@"logsDirectory=%@",fileLogger.logFileManager.logsDirectory);
//logs文件夹的所有log文件路径
DDLogInfo(@"sortedLogFilePaths=%@",fileLogger.logFileManager.sortedLogFilePaths);
//当前活跃的log文件路径
DDLogInfo(@"currentFilePath=%@",fileLogger.currentLogFileInfo.filePath);
log文件夹和文件名的自定义
我们项目中需要将log文件夹的内容上传到服务器。默认的Log文件存放在沙盒的Library/Caches/Logs
目录中,我想要在Logs文件夹之前加一个文件夹,便于存放上传的相关设置。也就是说想要把log文件的目录改为沙盒的Library/Caches/SQLog/Logs
目录中。
这个在创建DDFileogger
的时候可以进行设置。
//修改Logs文件夹的位置
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *baseDir = paths.firstObject;
NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"SQLog/Logs"];
DDLogFileManagerDefault *defaultManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:logsDirectory];
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:defaultManager];
还有个问题,我想要修改log文件名字,默认的log名字是
,例如com.organization.myapp 2013-12-03 17-14.log
。中间会有个空格,我担心在处理的过程中空格可能会出什么不可预知的问题,所以不想要这个空格(就看这个空格不爽,哈哈)。该如何修改这个log的名字呢。
这个就没有那么容易了,需要
- 1.新建一个继承于
DDLogFileManagerDefault
的类(姑且命名为SQFileManager), - 2.然后重写
- (NSString *)newLogFileName
方法和- (BOOL)isLogFile:(NSString *)fileName
方法。
#import "SQLogFileManager.h"
@implementation SQLogFileManager
//重写方法(log文件名生成规则)
- (NSString *)newLogFileName {
NSString *timeStamp = [self getTimestamp];
return [NSString stringWithFormat:@"%@.log", timeStamp];
}
//重写方法(是否是log文件)
- (BOOL)isLogFile:(NSString *)fileName {
BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
return hasProperSuffix;
}
- (NSString *)getTimestamp {
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter;
dispatch_once(&onceToken, ^{
dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:@"YYYY.MM.dd-HH.mm.ss"];
});
return [dateFormatter stringFromDate:NSDate.date];
}
@end
另外,在这个SQLogFileManager类里面,我们还可以重写- (NSString *)defaultLogsDirectory
方法,来修改Log文件夹路径
//重写方法(log文件夹路径)
- (NSString *)defaultLogsDirectory {
#if TARGET_OS_IPHONE
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *baseDir = paths.firstObject;
NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"SQLog/Logs"];
#else
NSString *appName = [[NSProcessInfo processInfo] processName];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? paths[0] : NSTemporaryDirectory();
NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"SQLog/Logs"] stringByAppendingPathComponent:appName];
#endif
return logsDirectory;
}
- 3.在创建fileLogger对象的时候设置我们自定义的logFileManager
这样就可以修改文件夹目录位置和log文件名字了。
//使用自定义的logFileManager
SQLogFileManager *fileManager = [[SQLogFileManager alloc] init];
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
自定义log文件的log输出格式
跟之前的DDOSLogger自定义输出格式一样,我们想要自定义log文件的输出格式。我们可以用之前的SQLogFormatter类,但是,这个格式里面并没有时间戳。。。尴尬了,所以还是得自定义一个。方法跟之前一样,创建一个类,遵循DDLogFormatter协议,然后实现- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
方法
#import "SQFileLogformatter.h"
@implementation SQFileLogformatter
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
NSString *logLevel = nil;
switch (logMessage->_flag) {
case DDLogFlagError:
logLevel = @"Error";
break;
case DDLogFlagWarning:
logLevel = @"Warn";
break;
case DDLogFlagInfo:
logLevel = @"Info";
break;
case DDLogFlagDebug:
logLevel = @"Debug";
break;
default:
logLevel = @"Verbose";
break;
}
NSString *formatLog = [NSString stringWithFormat:@"%@[SQ]%@%@-%ld %@",logMessage->_timestamp, logLevel,logMessage->_function,logMessage->_line,logMessage->_message];
return formatLog;
}
- (NSString *)getTimeStringWithDate:(NSDate *)date {
static NSDateFormatter *dateFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:@"YYYY.MM.dd-HH.mm.ss.SSS"];
});
return [dateFormatter stringFromDate:date];
}
@end
然后设置fileLogger的formatter就可以了。
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
fileLogger.logFormatter = [[SQFileLogformatter alloc] init];
清空日志
项目有个需求,就是测试期间需要清空文件里面的日志,便于查看后面某个操作的日志。这个功能也可以实现,只是实现起来会跟想象中不太一样。清空是一个破坏性的操作,删除一个东西之后想要找回来就不容易了。所以没有找到CocoaLumberjack有类似的接口提供。但是灵活一点,我们不就是不想看到前面的日志吗?直接生成一个新的日志文件不就可以了?清清白白、干干净净,完美。
//生成一个新的日志文件,之后的日志写入到新的文件
[_fileLogger rollLogFileWithCompletionBlock:^{
DDLogInfo(@"rollLogFileWithCompletionBlock");
}];
关闭log显示
在APP运行过程中,我们想要关闭log。
有两种方案:一种方案是将ddLogLevel的值改为DDLogLevelOff。这样对所有的Logger都会生效,不会有输出。
另外一种方案就是删除Logger。
//删除某个Logger
[DDLog removeLogger:osLogger];
//删除所有Logger
//[DDLog removeAllLoggers];
整合
经过上面的积累,我们就可以将这些功能都整合到一个管理类里面进行处理了。
#import
#import
//声明外部变量
extern DDLogLevel ddLogLevel;
// 默认的宏,方便使用
#define Log(frmt, ...) LogI(frmt, ##__VA_ARGS__)
// 提供不同的宏,对应到特定参数的对外接口
#define LogE(frmt, ...) DDLogError(frmt, ##__VA_ARGS__)
#define LogW(frmt, ...) DDLogWarn(frmt, ##__VA_ARGS__)
#define LogI(frmt, ...) DDLogInfo(frmt, ##__VA_ARGS__)
#define LogD(frmt, ...) DDLogDebug(frmt, ##__VA_ARGS__)
#define LogV(frmt, ...) DDLogVerbose(frmt, ##__VA_ARGS__)
@interface SQLogManager : NSObject
//初始化
+ (void)start;
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
//添加控制台logger
- (void)addOSLogger;
//添加文件写入Logger
- (void)addFileLogger;
//移除控制台logger
- (void)removeOSLogger;
//移除文件写入Logger
- (void)removeFileLogger;
#pragma mark - 等级切换
//切换log等级
- (void)switchLogLevel:(DDLogLevel)logLevel;
#pragma mark - 文件Log操作
- (void)createAndRollToNewFile;
//所有log文件路径
- (NSArray *)filePaths;
//当前活跃的log文件路径
- (NSString *)currentFilePath;
@end
#import "SQLogManager.h"
#import "SQLogFormatter.h"
#import "SQFileLogFormatter.h"
#import "SQLogFileManager.h"
//设置默认的log等级
#ifdef DEBUG
DDLogLevel ddLogLevel = DDLogLevelDebug;
#else
DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
@interface SQLogManager ()
@property (nonatomic, strong)DDFileLogger *fileLogger;//控制台logger
@property (nonatomic, strong)DDOSLogger *osLogger;//文件写入Logger
@end
@implementation SQLogManager
+ (void)start {
[self sharedInstance];
}
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
#pragma mark - Logger添加及删除
//添加控制台logger
- (void)addOSLogger {
[DDLog addLogger:self.osLogger];
}
//添加文件写入Logger
- (void)addFileLogger {
[DDLog addLogger:self.fileLogger];
}
//移除控制台logger
- (void)removeOSLogger {
[DDLog removeLogger:_osLogger];
}
//移除文件写入Logger
- (void)removeFileLogger {
[DDLog removeLogger:_fileLogger];
}
#pragma mark - 等级切换
//切换log等级
- (void)switchLogLevel:(DDLogLevel)logLevel {
ddLogLevel = logLevel;
}
#pragma mark - 文件Log操作
- (void)createAndRollToNewFile {
//achive现在的文件,新生成一个文件,并将以后的log写入新文件
[_fileLogger rollLogFileWithCompletionBlock:^{
NSLog(@"rollLogFileWithCompletionBlock");
}];
}
//所有log文件路径
- (NSArray *)filePaths {
return _fileLogger.logFileManager.sortedLogFilePaths;
}
//当前活跃的log文件路径
- (NSString *)currentFilePath {
NSString *filePath = _fileLogger.currentLogFileInfo.filePath;
return filePath;
}
#pragma mark - 懒加载
//控制台logger
- (DDOSLogger *)osLogger {
if (!_osLogger) {
_osLogger = [DDOSLogger sharedInstance];
//自定义输出格式
_osLogger.logFormatter = [[SQLogFormatter alloc] init];
}
return _osLogger;
}
//文件写入Logger
- (DDFileLogger *)fileLogger {
if (!_fileLogger) {
//使用自定义的logFileManager
SQLogFileManager *fileManager = [[SQLogFileManager alloc] init];
_fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
//使用自定义的Logformatter
_fileLogger.logFormatter = [[SQFileLogFormatter alloc] init];
//重用log文件,不要每次启动都创建新的log文件(默认值是NO)
_fileLogger.doNotReuseLogFiles = NO;
//log文件在24小时内有效,超过时间创建新log文件(默认值是24小时)
_fileLogger.rollingFrequency = 60*60*24;
//log文件的最大3M(默认值1M)
_fileLogger.maximumFileSize = 1024*1024*3;
//最多保存7个log文件(默认值是5)
_fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
//log文件夹最多保存10M(默认值是20M)
_fileLogger.logFileManager.logFilesDiskQuota = 1014*1024*10;
}
return _fileLogger;
}
@end
参考
本文Demo
CocoaLumberjack Github地址
浅谈iOS日志收集系统
日志库CocoaLumberjack
的整合过程
iOS平台常见日志库简介