集成背景
Swift自带的print打印日志方式确实是内容变简单了,没有了时间显示在调试某些功能或者bug的时候确实是个很头疼的事情。其次简单的print达不到我们目前的需求,我们需要APP有收集日志和保存到本地并能够分享出来的功能。然后基于CocoaLumberJack日志库的强大选择了此库集成日志收集功能。
基本使用
使用cocoapod安装库:pod 'CocoaLumberjack' .安装好之后,打开项目还不能直接使用DDLog的功能,首先因为日志库对于的各种打印使用Define的方式,swift直接使用不是行的(后面发现是可以使用swift版本的,会在我的另一篇文章写出来
)。于是我在项目中新建了一个OC的管理类DDLogWrapper去实现DDLog的打印功能。在swift首次新建的OC类会提醒你创建一个桥接文件点确定就行了,如果是已经有的就不需要创建了,创建好类之后放到桥接文件里
#ifndef DDLogDemo-Bridging-Header.h
#define DDLogDemo-Bridging-Header.h
#import "DDLogWrapper.h"
#endif
在类里面声明好接口
#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
NS_ASSUME_NONNULL_BEGIN
@interface DDLogWrapper : NSObject
+ (void)logError:(NSString *)message;
+ (void)logWarn:(NSString *)message;
+ (void)logInfo:(NSString *)message;
+ (void)logDebug:(NSString *)message;
+ (void)logVerbose:(NSString *)message;
@end
NS_ASSUME_NONNULL_END
然后在实现里面调用DDLog的各个等级日志打印功能
#import "DDLogWrapper.h"
// 定义Log等级
static const DDLogLevel ddLogLevel = DDLogLevelDebug;
@implementation DDLogWrapper
+(void)logError:(NSString *)message{
DDLogError(@"%@",message);
}
+(void)logWarn:(NSString *)message{
DDLogWarn(@"%@",message);
}
+(void)logInfo:(NSString *)message{
DDLogInfo(@"%@",message);
}
+(void)logDebug:(NSString *)message{
DDLogDebug(@"%@",message);
}
+(void)logVerbose:(NSString *)message{
DDLogVerbose(@"%@",message);
}
@end
然后我们来到Appdelegate里面将其初始化就可以使用了
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 添加logger 下面会说明这个DDOSLogger
DDLog.add(DDOSLogger.sharedInstance)
DDLogWrapper.logError("你好")
DDLogWrapper.logWarn("我不好")
DDLogWrapper.logInfo("为什么不好")
DDLogWrapper.logDebug("就是不太好")
DDLogWrapper.logVerbose("你需要治治")
return true
}
现在打印是有日志时间了,但这里会有人会疑惑了,代码里不是打印了5个吗?为什么控制台只输出了4句,最后一句没有打印?其实我们在创建DDLogWrapper的时候定义了一个log的等级,就是这句:
// 定义log等级
static const DDLogLevel ddLogLevel = DDLogLevelDebug;
CocoaLumberjack提供了不同等级的Log输出,一共有5种等级,以下为5种等级的宏定义。
DDLogError(frmt, ...) //错误 当ddLogLevel = DDLogLevelError及以上等级时会输出
DDLogWarn(frmt, ...) //警告 当ddLogLevel = DDLogLevelWarning及以上等级时会输出
DDLogInfo(frmt, ...) //信息 当ddLogLevel = DDLogLevelInfo及以上等级时会输出
DDLogDebug(frmt, ...) //调试 当ddLogLevel = DDLogLevelDebug及以上等级时会输出
DDLogVerbose(frmt, ...) //详细 当ddLogLevel = DDLogLevelVerbose及以上等级时会输出
输出的等级由我们在DDLogWrapper定义的ddLogLevel
决定。比如我们上面定义了ddLogLevel = DDLogLevelDebug
,那么debug及以下等级的Log都能够输出,而使用DDLogVerbose
的Log就不会输出。
logger记录器
在上面的demo里我们在项目入口初始化了一个DDOSLogger
// 添加logger 下面会说明这个DDOSLogger
DDLog.add(DDOSLogger.sharedInstance)
CocoaLumberjack内置了以下几种Logger。
DDFileLogger:是将log写入到文件中。这也是本篇我们着重要讲的
DDOSLogger:在iOS10开始使用,在将Log输出到 控制台.app 和 Xcode控制台。
DDASLLogger:将日志写入到控制台.app中。在iOS10开始过时
DDTTYLogger:将日志写入到Xcode控制台。
所以,想要替换Swift的Print,官方推荐的做法是:
在iOS10及以上系统版本,使用DDOSLogger
。
在iOS10以下版本,使用DDASLLogger+DDTTYLogger
。
写入文件
之前介绍了DDFileLogger
可以将日志写入到文件。添加DDFileLogger
就可以将日志记录到文件里面了,跟添加DDOSLogger
一样。
let fileLogger = DDFileLogger.init()
DDLog.add(fileLogger)
默认的log日志在沙盒的Library/Caches/Logs
目录中,是一个以.log为后缀的文本文件,如下图所示。
还可以给
DDFileLogger
添加各种属性,下面为属性设置都有解释
+ (void)addFileLogger{
DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
//重用log文件,不要每次启动都创建新的log文件(默认值是NO)
fileLogger.doNotReuseLogFiles = NO;
//log文件在24小时内有效,超过时间创建新log文件(默认值是24小时)
fileLogger.rollingFrequency = 60*60*24;
//禁用文件大小滚动
fileLogger.maximumFileSize = 0;
//最多保存7个log文件
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
//log文件夹最多保存20M
fileLogger.logFileManager.logFilesDiskQuota = 1024*1024*20;
[DDLog addLogger:fileLogger];
}
我这里将DDFileLogger
和DDOSLogger
在DDLogWrapper
里开了一个添加的方法在Appdelegate调用,如下
DDLogWrapper.addOSLogger()
DDLogWrapper.addFileLogger()
重点解释下rollingFrequency
和maximumFileSize
这两个参数在官方的API里也有举例说明
*For example:
*TherollingFrequency
is 24 hours,
*but the log file surpasses themaximumFileSize
after only 20 hours.
*The log file will be rolled at that 20 hour mark.
*A new log file will be created, and the 24 hour timer will be restarted.
*You may optionally disable rolling due to filesize by settingmaximumFileSize
to zero.
*If you do so, rolling is based solely onrollingFrequency
.
*You may optionally disable rolling due to time by settingrollingFrequency
to zero (or any non-positive number).
*If you do so, rolling is based solely onmaximumFileSize
.
*If you disable bothmaximumFileSize
androllingFrequency
, then the log file won't ever be rolled.
*This is strongly discouraged.
意思就是maximumFileSize
和 rollingFrequency
都用于管理滚动(创建新的日志文件)。无论哪个先发生,都会导致日志文件被滚动。官方举例说明了:如果设置rollingFrequency
是 24 小时,但日志日志文件仅在 20 小时后就超过了maximumFileSize
。日志文件将在该 20 小时标记处滚动将创建一个新的日志文件,并重新启动 24 小时计时器。反之也是一样。如果24小时没有达到 maximumFileSize
,也会创建一个新的日志文件重新计算。我们可以将 maximumFileSize
设置为零来选择禁用由于文件大小而导致的滚动,滚动仅基于rollingFrequency
。同样也可以通过将 rollingFrequency
设置为零(或任何非正数)来选择性地禁用滚动。这样做,滚动仅基于maximumFileSize
。如果同时禁用 maximumFileSize
和 rollingFrequency
,日志文件将永远不会被滚动。官方强烈建议不要这样做。
日志路径与文件名称的自定义
上面说到默认的log日志在沙盒的Library/Caches/Logs
目录中,是一个以.log为后缀的文本文件。我们现在项目需要在系统的文件APP
中可以看到然后可以分享出来,那么我们需要做的以下的事情:
1.首先需要将Log的路径改到Document里面。不然将日志文件放在Library里面我们在文件app中是看不到这个日志文件的。
//修改Logs文件夹的位置
NSString *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *logsDirectory = [paths stringByAppendingPathComponent:@"Logs"];
DDLogFileManagerDefault *defaultManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:logsDirectory];
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:defaultManager];
在创建DDFileLogger
的时候可以将DDLogFileManagerDefault
设置好路径后给DDFileLogger
初始化带上这个值就可以改变文件默认存储的位置。
2.要想在文件APP显示出来日志文件还需要Info plist
里面设置一下:Supports Document Browser
为 YES 或者 设置 Application supports iTunes file sharing
和 Supports opening documents in place
这两个为YES,然后就可以在文件中查看到日志文件:在文件APP选择我的iPhone
->your app文件夹
->Logs
就可以看到日志文件了。这里的Logs是我自己在
NSString *logsDirectory = [paths stringByAppendingPathComponent:@"Logs"];
这里设置的,随自己定义路径,可以加层级也可以直接放在Document下面。文件如图所示
选中日志文件长按然后点击共享就可以分享到微信等APP了。
为了用户更好的明白这个日志文件,我们可以修改日志的文件名称。默认的log名字是 <bundle identifier><date><time>.log
,例如com.CocoaLumberJack.DDLogDemo2021-08-12 13-48.log
。修改日志文件名称官方也作了说明:
*If you wish to change default filename, you can override following two methods.
*-newLogFileName
method would be called on new logfile creation.
*-isLogFile
method would be called to filter log files from all other files in logsDirectory.
*You have to parse given filename and return YES if it is logFile.
意思就是如果想要改变文件名称需要重写newLogFileName
和isLogFile
这两个方法是在DDLogFileManagerDefault
这个类里面,所以新建了一个类DDLogFileManagerSub
继承这个DDLogFileManagerDefault
然后重写这两个方法
#import "DDLogFileManagerSub.h"
@implementation DDLogFileManagerSub
- (NSString *)newLogFileName{
NSString *disPlayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
NSString *timeStamp = [self getTimeStamp];
return [NSString stringWithFormat:@"%@ %@.log", disPlayName, timeStamp];
}
- (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
- (NSString *)newLogFileName
就会返回一个新的日志文件名称,自已定义,我是用app名称和时间戳拼接成文件名。然后我们还需要做一件事就是把之前设置fileLogger
DDLogFileManagerDefault *defaultManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:logsDirectory];
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:defaultManager];
第一句改成
DDLogFileManagerDefault *defaultManager = [[DDLogFileManagerSub alloc] initWithLogsDirectory:logsDirectory];
DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:defaultManager];
这样生成日志文件之后文件名就会变成下图
日志格式自定义
打开上面的日志文件可以看到打印的日志内容,如图
但我想看更详细的格式以方便定位问题,比如说函数的名称,行数等等。其中还有个问题就是框架里给我返回的时间戳是少了8个小时的。这里我用的是模拟器左上角的时间没有用24小时制的时间是下午3点42,而日志格式的时间是差了8个小时的(不知道你们用这个框架的时候会不会有这个问题,反正我是遇到了)问题先放会,后面会说解决方法,先说如何自定义日志输出格式。框架很齐全,只需要让之前创建的管理类
DDLogWrapper
遵守DDLogFormatter
协议,然后实现一个协议方法- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
如下
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage{
NSString *formatLog = [NSString stringWithFormat:@"%@%@ line:%ld %@",logMessage->_timestamp, logMessage->_function,logMessage->_line,logMessage->_message];
return formatLog;
}
这里看下这个DDLogMessage的成员变量
@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;
#if DD_LEGACY_MESSAGE_TAG
id _tag __attribute__((deprecated("Use _representedObject instead", "_representedObject")));;
#endif
id _representedObject;
DDLogMessageOptions _options;
NSDate * _timestamp;
NSString *_threadID;
NSString *_threadName;
NSString *_queueLabel;
NSUInteger _qos;
}
我用了几个参数:timestamp
、fuction
、line
以及打印内容message
.分别表示时间戳
、方法
、行数
、打印内容
大家可以根据自己需求增减打印参数,到这里还要把这个formatter赋值给DDFileLogger
,然后为了解决时间问题我在协议方法调整了DDLogMessage
的timestamp
完整的demo如下
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage{
logMessage->_timestamp = [self getLocalDate];
NSString *formatLog = [NSString stringWithFormat:@"%@%@ line:%ld %@",logMessage->_timestamp, logMessage->_function,logMessage->_line,logMessage->_message];
return formatLog;
}
// 调整timestamp
- (NSDate *)getLocalDate{
NSDate *date = [NSDate date];
NSTimeZone *zone = [NSTimeZone systemTimeZone];
NSInteger interval = [zone secondsFromGMTForDate: date];
NSDate *localDate = [date dateByAddingTimeInterval: interval];
return localDate;
}
然后将formatter赋值给fileLogger
// 设置日志格式
fileLogger.logFormatter = [[self alloc] init];
这里的self就是DDLogWrapper
因为他遵守了DDLogFormatter
协议以及实现了协议方法所以可以生成新的formatter。看下新的打印日志格式:
现在时间也对了,想要的看的东西也有了。但是这个集成方式有个缺点,我是在公用类里面加的类方法实现打印方式,所以所有的log都是在这个类里面实现的打印的行数和类名自然也都是这个类的, 所以定位不到别的类打印日志的位置和某个其他类的方法,所以我又调研了下CocoaLumberjack这个日志库 发现其实是有swift的扩展的,可以参考我的下一个文章Swift集成CocoaLumberJack日志库(二),会比本篇边的更简洁更明了。