前言
對(duì)于一個(gè)已經(jīng)上線的產(chǎn)品,如果項(xiàng)目沒有自己的Log系統(tǒng)纽什,產(chǎn)品在線上出現(xiàn)問題措嵌,那就只能抓瞎了,所以項(xiàng)目中有一套自己成熟的Log系統(tǒng)是至關(guān)重要的芦缰。本文主要利用CocoaLumberjack來教大家如何去搭建自己的Log系統(tǒng)企巢。當(dāng)然,每個(gè)項(xiàng)目有自己不同的業(yè)務(wù)邏輯让蕾,你們也可以根據(jù)自己項(xiàng)目的業(yè)務(wù)邏輯來修改就好浪规。
CocoaLumberjack是一個(gè)可以在iOS和Mac開發(fā)中使用的日志庫,使用很簡(jiǎn)單探孝,但很功能很強(qiáng)大笋婿。在這里就不多作介紹了,大家可以去官網(wǎng)上看 -->CocoaLumberjack
好了,現(xiàn)在該說說如何去構(gòu)建自己的Log系統(tǒng)了。
Log系統(tǒng)原理圖
先介紹一下主要構(gòu)成Log系統(tǒng)的四大類:
1兼砖、XZLogFormatter
該類主要是根據(jù)我們自己設(shè)定的格式輸出我們需要保存的log颈娜。
- 主要實(shí)現(xiàn)
DDLogFormatter
協(xié)議 - .m文件中重寫
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
方法。這個(gè)方法返回值是 NSString,就是最終 log 的消息體字符串。參數(shù) logMessage 是由 logger 發(fā)的一個(gè) DDLogMessage 對(duì)象。下面貼出方法內(nèi)部實(shí)現(xiàn)
/**
* 這里的格式: log打印時(shí)間 -> log等級(jí) -> app版本 -> 打印位置 -> log描述
*
*/
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage{
NSMutableDictionary *logDict = [NSMutableDictionary dictionary];
//取得文件名
NSString *locationString;
NSArray *parts = [logMessage->_file componentsSeparatedByString:@"/"];
if ([parts count] > 0)
locationString = [parts lastObject];
if ([locationString length] == 0)
locationString = @"No file";
logDict[@"logTime"] = [self timestampToNSString:[self getCurrentDate] formatter:@"MM/dd HH:mm:ss"];
logDict[@"logLevel"] = [self getLogLevelWithDDLogLevel:logMessage.flag];
logDict[@"location"] = [NSString stringWithFormat:@"%@:%lu(%@)", locationString, (unsigned long)logMessage.line, logMessage.function];
logDict[@"des"] = logMessage.message;
// app版本
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *app_Version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
NSString *logStr = [NSString stringWithFormat:@"%@ %@ version:%@ location:%@\nDes:%@\n",logDict[@"logTime"],logDict[@"logLevel"],app_Version,logDict[@"location"],logDict[@"des"]];
return logStr;
}
2捞慌、XZLogger
- 自定義 logger,繼承
DDAbstractDatabaseLogger
柬批。在初始化方法中啸澡,先設(shè)定一些基本參數(shù),并且添加一個(gè)UIApplicationWillResignActiveNotification的觀察者萝快,這主要作用是app切換到后臺(tái)會(huì)有通知锻霎,保存日志到沙盒。
- (instancetype)init {
self = [super init];
if (self) {
self.deleteInterval = 0;
self.maxAge = 0;
self.deleteOnEverySave = NO;
self.saveInterval = 60;
self.saveThreshold = 500;//當(dāng)未保存的log達(dá)到500條時(shí)揪漩,會(huì)調(diào)用db_save方法保存
//注冊(cè)app切換到后臺(tái)通知旋恼,保存日志到沙盒
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(saveLog)
name:@"UIApplicationWillResignActiveNotification"
object:nil];
}
return self;
}
- (void)saveLog {
dispatch_async(_loggerQueue, ^{
[self db_save];
});
}
- 重寫
- (BOOL)db_log:(DDLogMessage *)logMessage
方法,每次打 log 時(shí)奄容,db_log就會(huì)被調(diào)用冰更。該方法主要內(nèi)容是將 log 發(fā)給 formatter,將返回的 log 消息體字符串保存在緩存中昂勒。 db_log 的返回值告訴 DDLog 該條 log 是否成功保存進(jìn)緩存蜀细。
/**
* 每次打 log 時(shí),db_log會(huì)被調(diào)用
*
*/
- (BOOL)db_log:(DDLogMessage *)logMessage
{
if (!_logFormatter) {
//沒有指定 formatter
return NO;
}
if (!_logMessagesArray)
_logMessagesArray = [NSMutableArray arrayWithCapacity:500]; // saveThreshold只設(shè)置了500條
//利用 formatter 得到消息字符串戈盈,添加到緩存奠衔,當(dāng)調(diào)用db_save時(shí)谆刨,寫入沙盒
[_logMessagesArray addObject:[_logFormatter formatLogMessage:logMessage]];
return YES;
}
- 每隔1分鐘或者未寫入文件的log數(shù)達(dá)到 500 時(shí),db_save 就會(huì)自動(dòng)被調(diào)用归斤,這個(gè)時(shí)候我們就可以把存在緩存中的Log全部寫入沙盒文件了
/**
* 每隔1分鐘或者未寫入文件的log數(shù)達(dá)到 500 時(shí)痊夭,db_save 就會(huì)被調(diào)用
*
*/
- (void)db_save{
//判斷是否在 logger 自己的GCD隊(duì)列中
if (![self isOnInternalLoggerQueue])
NSAssert(NO, @"db_saveAndDelete should only be executed on the internalLoggerQueue thread, if you're seeing this, your doing it wrong.");
//如果緩存內(nèi)沒數(shù)據(jù),啥也不做
if ([_logMessagesArray count] == 0) {
return;
}
//獲取緩存中所有數(shù)據(jù)脏里,之后將緩存清空
NSArray *oldLogMessagesArray = [_logMessagesArray copy];
_logMessagesArray = [NSMutableArray arrayWithCapacity:0];
//用換行符她我,把所有的數(shù)據(jù)拼成一個(gè)大字符串
NSString *logMessagesString = [oldLogMessagesArray componentsJoinedByString:@"\n"];
//判斷有沒有文件夾,如果沒有迫横,就創(chuàng)建
NSString *createPath = [NSString stringWithFormat:@"%@/Logs/%@",DOCUMENTS_PATH,oyToStr(USERID)];
NSString *txtPath = [NSString stringWithFormat:@"%@/%@",createPath,self.fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:createPath]) {
if (![[NSFileManager defaultManager] fileExistsAtPath:txtPath]) {
[[NSFileManager defaultManager] createFileAtPath:txtPath contents:nil attributes:nil];
}
} else {
[[NSFileManager defaultManager] createDirectoryAtPath:createPath withIntermediateDirectories:YES attributes:nil error:nil];
[[NSFileManager defaultManager] createFileAtPath:txtPath contents:nil attributes:nil];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:txtPath];
[fileHandle seekToEndOfFile]; //將節(jié)點(diǎn)跳到文件的末尾
NSData* stringData = [[NSString stringWithFormat:@"\n%@",logMessagesString] dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle writeData:stringData]; //追加寫入數(shù)據(jù)
[fileHandle closeFile];
//跳過iCloud上傳
[self addSkipBackupAttributeToItemAtPath:txtPath];
}
3番舆、XZUserLogManager
該類有3個(gè)作用
- 刪除過期Log,目前我定的只保留3天的Log矾踱,多余的全部清除恨狈,時(shí)間可以隨意改,無所謂的呛讲。
#pragma mark - delete out of date user log
- (void)deleteOutOfDateLog {
NSString *logPath = [NSString stringWithFormat:@"%@/Logs/%@/",DOCUMENTS_PATH,oyToStr(USERID)];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd"];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT+0800"];;
//刪除過期的日志
NSDate *prevDate = [[NSDate date] dateByAddingTimeInterval:-60*60*24*3];
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:prevDate];
[components setHour:0];
[components setMinute:0];
[components setSecond:0];
//要?jiǎng)h除三天以前的日志(0點(diǎn)開始)
NSDate *delDate = [[NSCalendar currentCalendar] dateFromComponents:components];
NSArray *logFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logPath error:nil];
for (NSString *file in logFiles)
{
NSString *fileName = [file stringByReplacingOccurrencesOfString:@".txt" withString:@""];
fileName = [fileName stringByReplacingOccurrencesOfString:@"GQ_Log_" withString:@""];
NSDate *fileDate = [dateFormatter dateFromString:fileName];
if (nil == fileDate)
{
continue;
}
if (NSOrderedAscending == [fileDate compare:delDate])
{
[[NSFileManager defaultManager] removeItemAtPath:[logPath stringByAppendingString:file] error:nil];
}
}
}
- 動(dòng)態(tài)修改Log等級(jí)和上傳Log到服務(wù)器拴事,這里需要說明一點(diǎn),我們項(xiàng)目是有即時(shí)通訊的圣蝎,所有需要修改Log等級(jí),只需要給用戶推一條消息衡瓶,然后用戶收到消息后徘公,就可以去修改Log等級(jí)了。后面說到
XZDynamicLogLevel
類的時(shí)候哮针,會(huì)詳細(xì)說明关面。所以上傳Log也是會(huì)給用戶推消息,用戶就后臺(tái)自動(dòng)上傳Log到服務(wù)器了十厢。所以你們看見的XZMessageModel
對(duì)象就是我們項(xiàng)目中的消息體對(duì)象等太,這一塊得根據(jù)你們具體的業(yè)務(wù)來設(shè)計(jì)。
/**
* 接收到服務(wù)器下發(fā)的日志消息
*
*/
- (void)adjustLogLevelWithMsgModel:(XZMessageModel *)msgModel {
//解析出來日志消息體
XZUserLogMsgModel *userLogModel = msgModel.content;
switch (userLogModel.actionType) {
case XZ_USER_LOG_ACTION_TYPE_LogLevel: { // 修改用戶日志等級(jí)
self.logLevel = userLogModel.logLevel;//先改變Log等級(jí)
[XZDynamicLogLevel ddSetLogLevel:(DDLogLevel)userLogModel.logLevel];//存數(shù)據(jù)庫
break;
}
case XZ_USER_LOG_ACTION_TYPE_GetLog: { // 上傳日志
//獲取日志本地路徑
NSString *txtPath = [NSString stringWithFormat:@"%@/Logs/%@/",DOCUMENTS_PATH,oyToStr(USERID)];
//獲取壓縮用戶日志到本地的路徑
NSString *zipPath = [NSString stringWithFormat:@"%@/Logs",DOCUMENTS_PATH];
NSString *zipName = [NSString stringWithFormat:@"%@%lld.zip",oyToStr(USERID),[self getCurrentDate]];
zipPath = [zipPath stringByAppendingPathComponent:zipName];
//壓縮用戶日志
BOOL success = [SSZipArchive createZipFileAtPath:zipPath withContentsOfDirectory:txtPath withPassword:userLogModel.compressPwd];
if (success) {
// 在這里上傳
}
break;
}
default:
break;
}
}
4蛮放、XZDynamicLogLevel
- 該類就是動(dòng)態(tài)調(diào)整log等級(jí)缩抡,先遵守
DDRegisteredDynamicLogging
協(xié)議 - 重寫
+ (void)ddSetLogLevel:(DDLogLevel)level
方法。收到動(dòng)態(tài)修改Log等級(jí)的消息后包颁,就取出消息中給的Log等級(jí)瞻想,然后存數(shù)據(jù)庫
+ (void)ddSetLogLevel:(DDLogLevel)level {
[[XZUserKVStoreManage shareInstance] saveNumberWithNumber:@(level)
TableName:XZ_USER_LOG_LEVEL_DB_NAME
key:XZ_USER_LOG_LEVEL_DB_KEY];
}
- 重寫
+ (DDLogLevel)ddLogLevel
方法,每次打印log會(huì)獲取log等級(jí)娩嚼,也就是會(huì)走+ (DDLogLevel)ddLogLevel
方法蘑险,在ddLogLevel
方法中,我們每次都會(huì)去獲取我們保存的Log等級(jí)
/**
* 每次打印log會(huì)獲取log等級(jí)
*
*/
+ (DDLogLevel)ddLogLevel {
NSInteger logLevel = [[XZUserLogManager shareInstance] getLogLevel];
if (logLevel == 0) {
#if DEBUG
return DDLogLevelVerbose;
#else
return DDLogLevelWarning;
#endif
} else {
if (logLevel == 1) {
return DDLogLevelError;
}
else if (logLevel == 2) {
return DDLogLevelWarning;
}
else if (logLevel == 3) {
return DDLogLevelInfo;
}
else if (logLevel == 4) {
return DDLogLevelDebug;
}
else {
return DDLogLevelVerbose;
}
}
}
5岳悟、AppDelegate
- AppDelegate中也需要做一些設(shè)置
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
// 啟動(dòng)日志
//刪除過期日志
[[XZUserLogManager shareInstance] deleteOutOfDateLog];
// 添加DDASLLogger佃迄,你的日志語句將被發(fā)送到Xcode控制臺(tái)
[DDLog addLogger:[DDTTYLogger sharedInstance]];
// 添加DDTTYLogger泼差,你的日志語句將被發(fā)送到Console.app
[DDLog addLogger:[DDASLLogger sharedInstance]];
XZLogger *logger = [[XZLogger alloc] init];
[logger setLogFormatter:[[XZLogFormatter alloc] init]];
[DDLog addLogger:logger];
DDLogInfo(@"%@",DOCUMENTS_PATH);
DDLogInfo(@"打印info");
DDLogWarn(@"打印Warn");
DDLogError(@"打印Error");
return YES;
}
這就是保存到沙盒的Log文件
582481A5-EA34-4A1A-85E0-63F5FB09E3C7.png
好了,收工呵俏。Demo在這里堆缘。