前言
CocoaLumberjack是適用于Mac和iOS的快速,簡單,功能強大且靈活的日志記錄框架。
最近想要升級項目中的Log庫,比較了一番之后,發現CocoaLumberjack這個庫可自定義性特別強,能夠滿足我們的所有要求,而且使用人數很多,可維護性強,性能也不錯。于是最終選擇了這個庫進行集成。集成這個庫的過程也花了一段時間,下面會用一步一步介紹這個庫的使用。最后會將這些功能集成到一個文件里面,方便使用。(話說好想吐槽下這個庫的名字。。。)
本文Demo
基本使用
使用cocoapod安裝庫:pod 'CocoaLumberjack','~> 3.6.1'
由于Log庫在所有的文件中都可能用到,故最好是在項目中的全局頭文件PCH文件中導入Log庫的頭文件。另外,這個庫有個挺特別的地方,需要我們自己定義一個ddLogLevel變量,用來定義當前日志的輸出等級。
//項目的pch文件中
#import <CocoaLumberjack/CocoaLumberjack.h>
//定義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 <CocoaLumberjack/CocoaLumberjack.h>
#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 <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;
}
- 接著我們需要設置
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名字是 <bundle identifier> <date> <time>.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 <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
//聲明外部變量
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平臺常見日志庫簡介