看了幾篇文章幕袱,一開始感覺整合第三方的日志庫CocoaLumberjack
比較簡單凌净,不過真正落地寻狂,發(fā)現(xiàn)還是有幾個地方需要注意的邓萨。
下載代碼
CocoaLumberjack/CocoaLumberjack
- 日志庫
CocoaLumberjack
目前已經(jīng)到了3.0.0
版本,既支持Object-C
锨天,也支持Swift
毯盈。包管理方面,既支持CocoaPods
病袄,也支持Carthage
搂赋。根據(jù)當(dāng)前工程現(xiàn)狀,選擇Object-C
+CocoaPods
的方式 - 在原有的Podfile中添加一行
pod 'CocoaLumberjack', '~> 3.0.0'
然后陪拘,將終端切換到工程目錄厂镇,執(zhí)行命令。帶上--no-repo-update
是為了加快更新速度
pod install --no-repo-update
基本需求
- 可以設(shè)定
Log
等級 - 可以積攢到一定量的
log
后左刽,一次性發(fā)送給服務(wù)器捺信,絕對不能打一個Log
就發(fā)一次 - 可以一定時間后帽驯,將未發(fā)送的
log
發(fā)送到服務(wù)器 - 可以在
App
切入后臺時將未發(fā)送的log
發(fā)送到服務(wù)器
利用 CocoaLumberjack 搭建自己的 Log 系統(tǒng)
這篇文章寫得很有代表性厢塘,這次也主要是按照這個來做。
接口設(shè)計
- 一般的文章卓缰,都介紹在
APPdelegate
中添加代碼喇辽,這會導(dǎo)致這個類很亂掌挚,不是很好 - 一版本的工程,都有自己的前綴菩咨,在工程里到處使用
DDLog
和整體氛圍不搭調(diào)吠式,最好在中間再包一層陡厘。 - 由于是日志,大家都習(xí)慣了
NSLog(frmt,...)
這種可變參數(shù)形式的c
風(fēng)格調(diào)用特占,而且一般還是宏定義的方式糙置。
引入一個單獨的類,采用(類方法 + 單例)的模式是目,簡化接口谤饭,保證只執(zhí)行一次
- 日志是一種服務(wù),所以懊纳,文件命名為
XXXLogService
揉抵,作為一個中間隔離層。用戶不需要知道日志系統(tǒng)是怎么實現(xiàn)的嗤疯,用了哪個第三方庫 - 提供一個類方法冤今,將初始化的代碼放在里面,在APPdelegate中只要一句調(diào)用就可以了茂缚,比如
[XXXLogService start];
- 以下是接口頭文件
XXXLogService.h
的內(nèi)容辟汰,將這個頭文件加入pch
文件中,就可以在工程里方便使用XXXLog()
了
#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
// 默認(rèn)的宏阱佛,方便使用
#define XXXLog(frmt, ...) XXXLogInfo(frmt, ...)
// 提供不同的宏,對應(yīng)到特定參數(shù)的對外接口
#define XXXLogError(frmt, ...) DDLogError(frmt, ##__VA_ARGS__)
#define XXXLogWarning(frmt, ...) DDLogWarn(frmt, ##__VA_ARGS__)
#define XXXLogInfo(frmt, ...) DDLogInfo(frmt, ##__VA_ARGS__)
#define XXXLogDebug(frmt, ...) DDLogDebug(frmt, ##__VA_ARGS__)
#define XXXLogVerbose(frmt, ...) DDLogVerbose(frmt, ##__VA_ARGS__)
@interface XXXLogService : NSObject
+ (void)start;
@end
- 以下是接口實現(xiàn)文件
XXXLogService.m
的內(nèi)容戴而,對外的類接口start
只是一層封裝凑术。具體的實現(xiàn)在一個成員函數(shù)中,這里用了單例所意,將只執(zhí)行一次的內(nèi)容放在了init
函數(shù)中淮逊。
這個類只是一層封裝,沒有做具體的事情扶踊。
內(nèi)容是從網(wǎng)上抄的泄鹏,這里準(zhǔn)備自定義一個loger
往后臺傳日志,下面那么注釋為沒必要的內(nèi)容秧耗,實際使用時就直接刪除了备籽。至于顏色控件相關(guān)設(shè)置,其實也沒有必要分井,這里只是做個備忘车猬。
#import "XXXLogService.h"
#import "XXXLogger.h"
#import "XXXLogFormatter.h"
@implementation XXXLogService
+ (void)start {
[self sharedInstance];
}
+ (instancetype)sharedInstance{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
// 自定義的log,要傳自己的后臺
XXXLogger *logger = [[XXXLogger alloc] init];
XXXLogFormatter *formatter = [[XXXLogFormatter alloc] init];
[logger setLogFormatter:formatter];
[DDLog addLogger:logger];
// 注釋中一些不需要的代碼只是放在這里尺锚,見證一下歷史珠闰;正式使用時都應(yīng)該刪除。
// XCode的log
// XCode8之后不支持插件工作瘫辩,所以這些設(shè)置顏色的代碼不需要伏嗜,否則將會有無用的顏色信息混入log
//開啟使用 XcodeColors
setenv("XcodeColors", "YES", 0);
//檢測
char *xcode_colors = getenv("XcodeColors");
if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) {
// XcodeColors is installed and enabled!
NSLog(@"XcodeColors is installed and enabled");
//開啟DDLog 顏色
[[DDTTYLogger sharedInstance] setColorsEnabled:YES];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor lightGrayColor] backgroundColor:nil forFlag:DDLogFlagVerbose];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor grayColor] backgroundColor:nil forFlag:DDLogFlagDebug];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor blueColor] backgroundColor:nil forFlag:DDLogFlagInfo];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor yellowColor] backgroundColor:nil forFlag:DDLogFlagWarning];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor redColor] backgroundColor:nil forFlag:DDLogFlagError];
}
// XCode的log坛悉,也用自定義的輸出格式
[[DDTTYLogger sharedInstance] setLogFormatter:formatter];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
// DDASLLogger是輸出到mac終端,沒有必要再手機上用
[DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs
// DDFileLogger是存在手機上承绸,在Cache目錄裸影,一般拿不出來,所以一般也沒什么大用
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
[DDLog addLogger:fileLogger];
}
return self;
}
- 刪除無用代碼八酒,精簡過后的
init
代碼如下
- (instancetype)init {
self = [super init];
if (self) {
// 自定義的log空民,要傳自己的后臺
XXXLogger *logger = [[WJSLogger alloc] init];
XXXLogFormatter *formatter = [[WJSLogFormatter alloc] init];
[logger setLogFormatter:formatter];
[DDLog addLogger:logger];
// XCode的log,也用自定義的輸出格式
[[DDTTYLogger sharedInstance] setLogFormatter:formatter];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
}
return self;
}
日志級別
- 需要用一個靜態(tài)全局變量來定義日志級別
- 日志級別是用來控制日志輸出的
- 通過日志
flag
(前面定義的)和日志級別level
(這里定義的)比較羞迷,決定是否輸出日志 - 日志級別
Error
最高Verbose
最低界轩,flag > level
就輸出,否則就不輸出
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
這樣定義的結(jié)果是:
DEBUG
模式:所有的日志都輸出
其他模式:僅僅DDLogError()
和 DDLogWarn()
輸出衔瓮,其他的都沒有輸出
- 這個變量的名字ddLogLevel最好命名為
ddLogLevel
浊猾,不然編譯不通過。原因是里面的宏定義用到了热鞍。
#ifndef LOG_LEVEL_DEF
#define LOG_LEVEL_DEF ddLogLevel
#endif
- 結(jié)合前面的宏定義葫慎,提供的方便方法,可以做到開發(fā)環(huán)境日志很全薇宠,而正式環(huán)境只搜集
warning
和error
兩種日志 - 在實現(xiàn)文件統(tǒng)一定義偷办,使用者不需要知道日志級別
level
這個概念,使用變得更簡單澄港。
// 默認(rèn)的宏椒涯,方便使用
#define XXXLog(frmt, ...) XXXLogInfo(frmt, ...)
自定義格式
- 如果需要自定義協(xié)議格式,那么需要實現(xiàn)
DDLogFormatter
協(xié)議的方法- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;
/**
* This protocol describes the behavior of a log formatter
*/
@protocol DDLogFormatter <NSObject>
@required
/**
* Formatters may optionally be added to any logger.
* This allows for increased flexibility in the logging environment.
* For example, log messages for log files may be formatted differently than log messages for the console.
*
* For more information about formatters, see the "Custom Formatters" page:
* Documentation/CustomFormatters.md
*
* The formatter may also optionally filter the log message by returning nil,
* in which case the logger will not log the message.
**/
- (NSString * __nullable)formatLogMessage:(DDLogMessage *)logMessage;
@end
- 下面是
DDLogMessage
的定義回梧,使用時一般用指針符號`->``引用內(nèi)部變量废岂,而給出的屬性都是可讀的,不能訪問狱意。不是非常理解這種設(shè)計意圖湖苞。
/**
* The `DDLogMessage` class encapsulates information about the log message.
* If you write custom loggers or formatters, you will be dealing with objects of this class.
**/
@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;
}
/**
* Default `init` is not available
*/
- (instancetype)init NS_UNAVAILABLE;
/**
* Standard init method for a log message object.
* Used by the logging primitives. (And the macros use the logging primitives.)
*
* If you find need to manually create logMessage objects, there is one thing you should be aware of:
*
* If no flags are passed, the method expects the file and function parameters to be string literals.
* That is, it expects the given strings to exist for the duration of the object's lifetime,
* and it expects the given strings to be immutable.
* In other words, it does not copy these strings, it simply points to them.
* This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters,
* so it makes sense to optimize and skip the unnecessary allocations.
* However, if you need them to be copied you may use the options parameter to specify this.
*
* @param message the message
* @param level the log level
* @param flag the log flag
* @param context the context (if any is defined)
* @param file the current file
* @param function the current function
* @param line the current code line
* @param tag potential tag
* @param options a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction.
* @param timestamp the log timestamp
*
* @return a new instance of a log message model object
*/
- (instancetype)initWithMessage:(NSString *)message
level:(DDLogLevel)level
flag:(DDLogFlag)flag
context:(NSInteger)context
file:(NSString *)file
function:(NSString *)function
line:(NSUInteger)line
tag:(id)tag
options:(DDLogMessageOptions)options
timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER;
/**
* Read-only properties
**/
/**
* The log message
*/
@property (readonly, nonatomic) NSString *message;
@property (readonly, nonatomic) DDLogLevel level;
@property (readonly, nonatomic) DDLogFlag flag;
@property (readonly, nonatomic) NSInteger context;
@property (readonly, nonatomic) NSString *file;
@property (readonly, nonatomic) NSString *fileName;
@property (readonly, nonatomic) NSString *function;
@property (readonly, nonatomic) NSUInteger line;
@property (readonly, nonatomic) id tag;
@property (readonly, nonatomic) DDLogMessageOptions options;
@property (readonly, nonatomic) NSDate *timestamp;
@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID
@property (readonly, nonatomic) NSString *threadName;
@property (readonly, nonatomic) NSString *queueLabel;
@end
- 下面這篇參考文章中,在自定義的
log
中顯示等級详囤、類和方法财骨、代碼行數(shù)是個不錯的做法。
使用CocoaLumberjack和XcodeColors實現(xiàn)分級Log和控制臺顏色
- (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 = @"[VBOSE] > ";
break;
}
NSString *formatLog = [NSString stringWithFormat:@"%@[%@ %@][line %ld] %@",
logLevel, logMessage->_fileName, logMessage->_function,
logMessage->_line, logMessage->_message];
return formatLog;
}
自定義的logger
根據(jù)網(wǎng)上內(nèi)容利用 CocoaLumberjack 搭建自己的 Log 系統(tǒng)修改而來
- 從類
DDAbstractDatabaseLogger
繼承而來藏姐,需要包含頭文件#import <CocoaLumberjack/DDAbstractDatabaseLogger.h>
- 這個類的作用是將log保存在數(shù)據(jù)庫中蚓再,這個類沒有暴露出來。相對來說包各,數(shù)據(jù)庫比文件系統(tǒng)操作要方便摘仅。所以用這個類,而不用文件類
DDFileLogger
- 保持默認(rèn)設(shè)置就好了问畅,達(dá)到500條或者間隔1分鐘就保存娃属;磁盤數(shù)據(jù)庫保留7天六荒,刪除操作間隔5分鐘。
- 保存在數(shù)據(jù)庫中內(nèi)容取得不方便矾端,所以手機數(shù)據(jù)庫中的內(nèi)容我們不關(guān)心掏击,他能正常工作就好了。
- 每一次執(zhí)行
log
秩铆,函數(shù)db_log
就會執(zhí)行砚亭。在這里,我們把每條log都保存在一個數(shù)組中殴玛。比如@property (nonatomic, strong) NSMutableArray *logs;
- 每一次保存
log
捅膘,函數(shù)db_save
就會執(zhí)行。在這里滚粟,我們把緩存在數(shù)組中的log
拼接成一個大字符串(\n
分隔)寻仗,發(fā)送給后臺。向后臺發(fā)送成功后凡壤,清空這個緩存數(shù)組署尤。 - 至于數(shù)據(jù)庫中的內(nèi)容,我們不用關(guān)心亚侠。
- 監(jiān)聽系統(tǒng)消息
UIApplicationWillResignActiveNotification
曹体,在應(yīng)用回到后臺前,保存一下硝烂,向服務(wù)器發(fā)送一次混坞。
#import "XXXLogger.h"
#import "MyAFNetWorking.h"
// 達(dá)到500條就發(fā)送,所以緩存的數(shù)組最大容量達(dá)到2000的話钢坦,說明網(wǎng)絡(luò)出了問題
#define kLogCacheCapacity 2000
@interface XXXLogger ()
@property (nonatomic, strong) NSMutableArray *logs;
@end
@implementation XXXLogger
// 生命周期函數(shù)
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (instancetype)init {
self = [super init];
if (self) {
self.logs = [NSMutableArray array];
// 使用默認(rèn)的配置。達(dá)到500條或者間隔1分鐘就保存啥酱;磁盤數(shù)據(jù)庫保留7天爹凹,刪除操作間隔5分鐘,這兩個數(shù)據(jù)不關(guān)心镶殷,用基類的就可以了
self.saveThreshold = 500; // 達(dá)到500條就保存?zhèn)骱笈_
self.saveInterval = 60; // 60s定時到就保存?zhèn)骱笈_
// 監(jiān)聽UIApplicationWillResignActiveNotification消息禾酱,在程序進(jìn)入后臺前保存log
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
}
return self;
}
// 重寫父類函數(shù)
- (BOOL)db_log:(DDLogMessage *)logMessage {
// _logFormatter只能用下劃線變量訪問,不能用self的方式绘趋,否則會觸發(fā)斷言
if (!_logFormatter) {
//沒有指定 formatter
return NO;
}
if ([self.logs count] > kLogCacheCapacity) {
// 如果段時間內(nèi)進(jìn)入大量log颤陶,并且遲遲發(fā)不到服務(wù)器上,我們可以判斷哪里出了問題陷遮,在這之后的 log 暫時不處理了滓走。
// 但我們依然要告訴 DDLog 這個存進(jìn)去了。
return YES;
}
//利用 formatter 得到消息字符串帽馋,添加到緩存
@synchronized (self) {
// _logFormatter只能用下劃線變量訪問搅方,不能用self的方式比吭,否則會觸發(fā)斷言
[self.logs addObject:[_logFormatter formatLogMessage:logMessage]];
}
return YES;
}
- (void)db_save {
//如果緩存內(nèi)沒數(shù)據(jù),啥也不做
if (0 == [self.logs count]) {
return;
}
// 用換行符姨涡,把所有的數(shù)據(jù)拼成一個大字符串
NSString *logsString = [self.logs componentsJoinedByString:@"\n"];
// 發(fā)送給服務(wù)器衩藤,將AFNetworking包一層作為網(wǎng)絡(luò)傳輸
NSString *url = @""; // 根據(jù)實際修改
NSDictionary *logs = @{@"log": logsString}; // key值跟后臺商量好
__weak __typeof(self) weakSelf = self;
[[MyAFNetWorking shareAfnetworking] performRequestWithPath:url formDataDic:logs success:^(NSDictionary *responseObject) {
// 已經(jīng)成功傳到服務(wù)器,之后將緩存清空
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf clearLogs];
} failure:^(NSDictionary *responseObject) {
// 啥也不做
}];
}
// selector
- (void)onWillResignActive:(NSNotification *)notification {
dispatch_async(self.loggerQueue, ^{
[self db_save];
});
}
// 清空緩存
- (void)clearLogs {
@synchronized (self) {
[self.logs removeAllObjects];
}
}
@end
日志顏色
支持XCodeColors插件涛漂,根據(jù)日志等級顯示不同的顏色赏表。不過XCode8之后插件都不能用了,所以就不用折騰了匈仗。
下面是一些文章鏈接瓢剿,可以看看
robbiehanson/XcodeColors
Xcode8 插件失效不能用
Xcode升級后插件失效解決辦法
CocoaLumberjack使用
日志保存到阿里云
- 日志可以發(fā)送到自己的后臺,怎么發(fā)送锚沸,和后臺商量好就行
- 阿里云提供了日志服務(wù)器跋选,提供了日志發(fā)送的API,可以將客戶端的日志直接發(fā)送到阿里云哗蜈。運維可以通過工具查看前标,這樣就繞過后臺,也減輕了后臺的壓力距潘。
-
lujiajing1126/AliyunLogObjc
這個是阿里提供的log
上傳接口炼列,目前的熱度還很低 - 支持
Carthage
,不支持CocoaPods
音比,這個有點特別俭尖。 - 如果工程還要支持
iOS7
,還是直接將源碼導(dǎo)入工程比較好洞翩,不需要庫管理工具稽犁。如果是Swift
開發(fā)的工程,用Carthage
就很方便骚亿。
#import <AliyunLogObjc/AliyunLogObjc.h>
LogClient *client = [[LogClient alloc] initWithApp: @"endpoint" accessKeyID:@"" accessKeySecret:@"" projectName:@""];
LogGroup *logGroup = [[LogGroup alloc] initWithTopic: @"" andSource:@""];
Log *log1 = [[Log alloc] init];
[log1 PutContent: @"Value" withKey: @"Key"];
[logGroup PutLog:log1];
[client PostLog:logGroup logStoreName: @"" call:^(NSURLResponse* _Nullable response,NSError* _Nullable error) {
if (error != nil) {
}
}];
- 僅有日志上傳功能已亥,并沒有日志本地管理功能±赐溃可以配合
CocoaLumberjack
使用虑椎。 - 如果只是簡單將日志發(fā)送阿里云,單獨使用也是可以的俱笛。