基本的方案
日志管理采用第三方庫CocoaLumberjack/CocoaLumberjack
后臺采用阿里云lujiajing1126/AliyunLogObjc
發(fā)送失敗的日志选调,存本地緩存虐沥,手機(jī)啟動或者從后臺進(jìn)入前臺的時候再發(fā)一次囤躁。本地緩存采用第三方庫ibireme/YYCache
接口函數(shù)
采用類似NSLog();
的形式,只是對第三方庫的一層簡單封裝
// 默認(rèn)的宏斥黑,方便使用
#define WJSLog(frmt, ...) WJSLogInfo(frmt, ##__VA_ARGS__)
// 提供不同的宏,對應(yīng)到特定參數(shù)的對外接口
#define WJSLogError(frmt, ...) DDLogError(frmt, ##__VA_ARGS__)
#define WJSLogWarning(frmt, ...) DDLogWarn(frmt, ##__VA_ARGS__)
#define WJSLogInfo(frmt, ...) DDLogInfo(frmt, ##__VA_ARGS__)
#define WJSLogDebug(frmt, ...) DDLogDebug(frmt, ##__VA_ARGS__)
#define WJSLogVerbose(frmt, ...) DDLogVerbose(frmt, ##__VA_ARGS__)
日志等級
- 按照第三方庫的分法眉厨,分為5個等級
typedef NS_OPTIONS(NSUInteger, DDLogFlag){
/**
* 0...00001 DDLogFlagError
*/
DDLogFlagError = (1 << 0),
/**
* 0...00010 DDLogFlagWarning
*/
DDLogFlagWarning = (1 << 1),
/**
* 0...00100 DDLogFlagInfo
*/
DDLogFlagInfo = (1 << 2),
/**
* 0...01000 DDLogFlagDebug
*/
DDLogFlagDebug = (1 << 3),
/**
* 0...10000 DDLogFlagVerbose
*/
DDLogFlagVerbose = (1 << 4)
};
-
DEBUG
模式全部輸出锌奴,其他模式只輸出DDLogFlagInfo
以上的日志
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
- 不論是
DEBUG
模式還是其他什么模式,DDLogLevelVerbose
和DDLogLevelDebug
兩種級別的日志都不發(fā)后臺
// 對于Debug和verbose級別的日志憾股,不發(fā)送到后臺
// 但我們依然要告訴 DDLog 這個存進(jìn)去了鹿蜀。
if ((DDLogLevelDebug == logMessage.flag) || (DDLogLevelVerbose == logMessage.flag)) {
return YES;
}
對第三方庫CocoaLumberjack
功能選擇
輸出到
XCode
控制臺的DDTTYLogger
需要保留XCode8
之后,插件基本上都被封了荔燎,所以顏色的插件不引入robbiehanson/XcodeColors輸出到控制臺的
DDASLLogger
是給MAC
開發(fā)用的耻姥,iOS
開發(fā)不適用,不導(dǎo)入有咨。文件系統(tǒng)在用戶手機(jī)上琐簇,除了占用戶手機(jī)空間,基本沒什么大用座享,所以
DDFileLogger
也不引入婉商。DDAbstractDatabaseLogger
,是寫入本地數(shù)據(jù)庫的一個類渣叛,不能直接用(具體的讀寫數(shù)據(jù)庫的類只定義了方法名丈秩,需要子類覆蓋),適合作為基類淳衙,自定義實現(xiàn)蘑秽。實際使用中繼承這個類饺著,進(jìn)行自定義。DDAbstractDatabaseLogger
有個很好的特性肠牲,就是可以定義當(dāng)日志達(dá)到多少條或者間隔時間達(dá)到多久幼衰,(這兩個是或的關(guān)系),他就調(diào)用函數(shù)- (void)db_save;
在這個函數(shù)里缀雳,我們可以自定義覆蓋渡嚣,在這里將日志批量發(fā)送給阿里云后臺。是一個數(shù)組肥印,數(shù)組的成員是一個字典识椰,一個字典代表一條日志。
#define kLogNumberThreshold 50 // 達(dá)到多少條就保存?zhèn)骱笈_
#define kLogTimeThreshold (5 * 60) // 間隔多少時間(單位:秒)就保存?zhèn)骱笈_
我們定義的是日志數(shù)量達(dá)到
50
條或者間隔時間達(dá)到5
分鐘深碱,就往阿里云批量發(fā)送一次腹鹉。
- (instancetype)init {
self = [super init];
if (self) {
// 自定義的log,直接處理格式莹痢,不用代理种蘸。阿里云要求的是字典,不是字符串
XXXLogger *logger = [[XXXLogger alloc] init];
[DDLog addLogger:logger];
// XCode的log竞膳,用自定義的輸出格式航瞭,采用代理的格式
XXXLogFormatter *formatter = [[XXXLogFormatter alloc] init];
[[DDTTYLogger sharedInstance] setLogFormatter:formatter];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
}
return self;
}
日志格式
沒有默認(rèn)提供的日志格式
提供了一個協(xié)議,來自定義日志的格式
自定義一個類坦辟,實現(xiàn)協(xié)議函數(shù)刊侯,添加一些必要的信息:比如日志等級,文件名锉走,函數(shù)名滨彻,行數(shù)等等
這個格式僅僅用在
XCode
的調(diào)試輸出,傳到阿里云后臺的格式在自定義類中直接寫挪蹭。那里要求的是一個字典亭饵,不是一個字符串。
- (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;
}
日志發(fā)送到阿里云
#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) {
}
}];
LogClient
一般只要一個就可以了梁厉,可以考慮用一個單例包裝一下辜羊。那些參數(shù)可以問運維拿,買了阿里云的服務(wù)之后词顾,一般都是有的八秃。LogGroup
本質(zhì)上是將一群log放在一個數(shù)組中,一次性發(fā)送肉盹。所以可以考慮以字符數(shù)組為入?yún)⑽羟盟b一下,然后調(diào)用LogClient
發(fā)送LogGroup
創(chuàng)建時需要兩個固定字段__topic__
和__source
上忍。__topic__
代表主題骤肛,對日志進(jìn)行分類纳本。這里的日志都是集中處理的,主要通過日志級別區(qū)分腋颠,主題很難給饮醇,所以統(tǒng)一給了個ios-log-native
。__source
代表日志來源秕豫,PC
上可以給ip
地址,手機(jī)上可以考慮給廣告id
观蓄,能標(biāo)識設(shè)備就可以了混移。當(dāng)然,這兩個字段給空字符串也沒什么問題Log
代表一條日志侮穿,是一個字典歌径。有些字段是固定的,比如時間亲茅,對應(yīng)的key
是__time__
回铛。其他的字段,跟運維商量克锣,這里填進(jìn)去就好了茵肃。比如文件名file_name
,函數(shù)名function
袭祟,行數(shù)line
等等約定一個名字验残,到時候方便歸類查找就可以了。
#define kLogKeyLevel @"level" // 日志等級
#define kLogKeyFileName @"file_name" // 文件名或者說是類名
#define kLogKeyFunction @"function" // 函數(shù)名或者說是方法名
#define kLogKeyLine @"line" // 行數(shù)
#define kLogKeyContent @"content" // 日志內(nèi)容
發(fā)送函數(shù)就是
PostLog
函數(shù)巾乳,成功和失敗的回調(diào)函數(shù)都在errorCallback
中您没。可以通過判斷參數(shù)error
是否為nil
來區(qū)分成功和失敗胆绊。這種設(shè)計真的很爛挑社。函數(shù)接口戈锻,現(xiàn)在都要求顯示指定是否為
nil
,如果用Carthage
管理,有warning
也不用管录淡,反正有framework
進(jìn)行隔離。不過随橘,我們這次是直接引入源文件的(要支持iOS7
)蕴坪,所以為了消除warning
,需要加上_Nullable
- (id _Nonnull )initWithApp:(NSString*_Nonnull) endPoint accessKeyID:(NSString *_Nonnull)ak accessKeySecret: (NSString *_Nonnull)as projectName: (NSString *_Nonnull)name;
- (void)PostLog:(LogGroup*_Nonnull)logGroup logStoreName:(NSString*_Nullable)name call:(void (^_Nullable)(NSURLResponse* _Nullable response,NSError* _Nullable error) )errorCallback;
-
關(guān)于證書:阿里云的接口碌廓,固定是
https
的传轰,不過我們的證書過期了,需要改成http
的谷婆。這個就要改阿里云接口的源碼LogClient.m
慨蛙。最好是用https
的辽聊,不要改源碼
- (void)PostLog:(LogGroup*)logGroup logStoreName:(NSString*)name call:(void (^)(NSURLResponse* _Nullable response,NSError* _Nullable error) )errorCallback {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Force to use https api interface
// Due to the requirement of Apple Security Policy after Jan. 1st, 2017
// NSString *httpUrl = [NSString stringWithFormat:@"https://%@.%@/logstores/%@/shards/lb",_mProject,_mEndPoint,name];
// 我們的阿里云服務(wù)不支持證書,這里改為http可以把log傳到后臺期贫,否則用https就是證書不通過跟匆。
NSString *httpUrl = [NSString stringWithFormat:@"http://%@.%@/logstores/%@/shards/lb",_mProject,_mEndPoint,name];
NSData *httpPostBody = [[logGroup GetJsonPackage] dataUsingEncoding:NSUTF8StringEncoding];
NSData *httpPostBodyZipped = [httpPostBody gzippedData];
NSDictionary<NSString*,NSString*>* httpHeaders = [self GetHttpHeadersFrom:name url:httpUrl body:httpPostBody bodyZipped:httpPostBodyZipped];
[self HttpPostRequest: httpUrl withHeaders:httpHeaders andBody:httpPostBodyZipped callback:errorCallback];
});
}
關(guān)于本地緩存
CocoaLumberjack
中有DDFileLogger
做手機(jī)的本地緩存,引入也很方便通砍。不過他沒有提供讀取和存儲文件日志的接口函數(shù)玛臂,日志格式也不符合阿里云的需求,所以不準(zhǔn)備用封孙。本地緩存迹冤,采用
YYCache
來自定義實現(xiàn),key-value
的格式虎忌,存取都很方便泡徙,并且還是id
類型值,NSString *
的key
膜蠢,非常合理的設(shè)計一般情況下堪藐,將
log
存用戶手機(jī)并沒有什么用,因為你總不能讓用戶的手機(jī)拿過來給你導(dǎo)一下日志吧挑围?所以大多數(shù)情況下只要將日志發(fā)送后臺就可以了礁竞。阿里云的接口,就是將一個數(shù)組發(fā)送出去杉辙,沒有涉及本地緩存的內(nèi)容在網(wǎng)絡(luò)異常情況下產(chǎn)生的日志苏章,無法發(fā)送阿里云。這種情況下奏瞬,本地緩存是有價值的枫绅。在網(wǎng)絡(luò)好的時候,再把這部分日志發(fā)送到后臺
本地緩存是
key-value
格式的硼端,對于日志來說并淋,很難定義這個key
,要能方便地定義這個key
是比較麻煩的珍昨,存取是分開的县耽,彼此并不知道對應(yīng)的key
。這里的方法是固定提供一定數(shù)量的位置镣典,命名規(guī)律化比如log0 ~ log999
兔毙,每一個位置對應(yīng)一個數(shù)組的日志。同時也只是正常發(fā)送失敗時的緩存兄春,應(yīng)該差不多了澎剥。可以考慮做一個定時器,定時將本地緩存發(fā)送到阿里云赶舆。這里定義的時間間隔是
1
小時哑姚。存
log
到本地緩存的方式是先查log0 ~ log999
哪個有空祭饭,哪個有空就用哪個key
,將內(nèi)容緩存到本地叙量。如果一圈下來都滿了倡蝙,就把最后一個覆蓋掉。
- (void)saveLogsToLocalCache:(NSArray *)logs {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
NSString *key = nil;
// 從0開始輪詢绞佩,直到查到一個有空的寺鸥;如果填滿了,就覆蓋最后一個
for (NSInteger i = 0; i < 1000; i++) {
key = [NSString stringWithFormat:@"Log%ld", (long)i];
if (![self.localCache containsObjectForKey:key]) {
break;
}
}
[self.localCache setObject:logs forKey:key];
}
});
}
- 取
log
的方式是定時輪詢(1
小時)品山,log0 ~ log999
都查一遍析既,只要有內(nèi)容,就取出來谆奥,發(fā)送到阿里云。如果發(fā)送成功就把對應(yīng)的logi
刪除掉拂玻。如果失敗酸些,就什么也不做(本來就已經(jīng)在本地緩存中)。等下一個周期來再試一次檐蚜。
- (void)sendLocalCacheLogsToAliyun {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
for (NSInteger i = 0; i < 1000; i++) {
NSString *key = [NSString stringWithFormat:@"WJSLog%ld", i];
NSArray *logs = (NSArray *)[self.localCache objectForKey:key];
if ((nil == logs) || (0 == logs.count)) {
continue;
}
[self sendLogsToAliyun:logs success:^(NSURLResponse * _Nullable response) {
[self.localCache removeObjectForKey:key];
} fail:nil];
}
}
});
}
日志發(fā)送
- 外面包一層魄懂,給出簡單的接口,方便使用
@interface XXXLogSender : NSObject
// 將logs數(shù)組發(fā)送到阿里云闯第,如果失敗市栗,會把這個logs存儲在本地緩存中(內(nèi)存緩存和磁盤緩存中都有)
+ (void)sendLogs:(NSArray *)logs;
@end
- 內(nèi)部調(diào)用了阿里云的接口進(jìn)行日志發(fā)送;調(diào)用
YYCache
的API
進(jìn)行發(fā)送失敗時的本地緩存咳短。這些都隱藏在實現(xiàn)文件內(nèi)部填帽。隱藏的方式是“類方法+單例”,對外提供簡潔的調(diào)用接口咙好。
#import "XXXLogSender.h"
#import "LogClient.h"
#import "LogGroup.h"
#import "Log.h"
#import <YYCache/YYCache.h>
#import <AdSupport/AdSupport.h>
#define kEndpoint @"" // 問運維要
#define kAccessKeyID @"" // 問運維要
#define kAccessKeySecret @"" // 問運維要
#define kProjectName @"" // 問運維要
#define kLogStoreName @"" // 問運維要
#define kLocalCacheName @"XXXLogCache" // 本地緩存的名字篡腌,YYCache要求的
// 留Log0 ~ Log999位置,填滿就覆蓋最后一個勾效。每一個位置是一個logs數(shù)組嘹悼。發(fā)送失敗暫存本地
#define kMaxLocalCacheNumber 1000
// 輪詢本地緩存,向阿里云發(fā)送日志的時間間隔
#define kSendLocalCacheLogsInterval (1 * 60 * 60)
@interface XXXLogSender ()
@property (nonatomic, strong) LogClient *client; // 阿里云日志發(fā)送對象
@property (nonatomic, strong) YYCache *localCache; // 本地緩存對象
@property (nonatomic, strong) dispatch_source_t timer; // 本地緩存發(fā)送定時器
@end
@implementation XXXLogSender
# pragma mark interface
+ (void)sendLogs:(NSArray *)logs {
[[XXXLogSender sharedInstance] sendLogsToAliyun:logs];
}
# pragma mark lifecycle
+ (instancetype)sharedInstance{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void)dealloc {
if (nil != self.timer) {
dispatch_cancel(self.timer);
self.timer = nil;
}
}
- (instancetype)init {
self = [super init];
if (self) {
self.client = [[LogClient alloc] initWithApp:kEndpoint accessKeyID:kAccessKeyID accessKeySecret:kAccessKeySecret projectName:kProjectName];
[self createTimer];
self.localCache = [YYCache cacheWithName:kLocalCacheName];
}
return self;
}
# pragma mark private
- (void)sendLogsToAliyun:(NSArray *)logs {
__weak __typeof(self)weakSelf = self;
[self sendLogsToAliyun:logs success:nil fail:^(NSError * _Nullable error) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
[strongSelf saveLogsToLocalCache:logs];
}];
}
- (void)sendLogsToAliyun:(NSArray *)logs success:(void(^)(NSURLResponse * _Nullable response))successCallback fail:(void(^)(NSError * _Nullable error))failCallback {
if ((nil == logs) || (0 == logs.count)) {
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *topic = @"ios-log-native"; // 自定義的,跟運維商量好
NSString *source = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString]; // 廣告id层宫,用來標(biāo)識來源手機(jī)杨伙,
LogGroup *group = [[LogGroup alloc] initWithTopic:topic andSource:source];
for (NSDictionary *sourceLog in logs) {
Log *log = [[Log alloc] init];
[log PutContent:sourceLog[kLogKeyLevel] withKey:kLogKeyLevel];
[log PutContent:sourceLog[kLogKeyFileName] withKey:kLogKeyFileName];
[log PutContent:sourceLog[kLogKeyFunction] withKey:kLogKeyFunction];
[log PutContent:sourceLog[kLogKeyLine] withKey:kLogKeyLine];
[log PutContent:sourceLog[kLogKeyContent] withKey:kLogKeyContent];
[group PutLog:log];
}
[self.client PostLog:group logStoreName:kLogStoreName call:^(NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (nil == error) {
if (nil != successCallback) {
successCallback(response);
}
} else {
if (nil != failCallback) {
failCallback(error);
}
}
}];
});
}
- (void)saveLogsToLocalCache:(NSArray *)logs {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
NSString *key = nil;
// 從0開始輪詢,直到查到一個有空的萌腿;如果填滿了限匣,就覆蓋最后一個
for (NSInteger i = 0; i < kMaxLocalCacheNumber; i++) {
key = [NSString stringWithFormat:@"WJSLog%ld", (long)i];
if (![self.localCache containsObjectForKey:key]) {
break;
}
}
[self.localCache setObject:logs forKey:key];
}
});
}
- (void)sendLocalCacheLogsToAliyun {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self) {
for (NSInteger i = 0; i < kMaxLocalCacheNumber; i++) {
NSString *key = [NSString stringWithFormat:@"WJSLog%ld", i];
NSArray *logs = (NSArray *)[self.localCache objectForKey:key];
if ((nil == logs) || (0 == logs.count)) {
continue;
}
[self sendLogsToAliyun:logs success:^(NSURLResponse * _Nullable response) {
[self.localCache removeObjectForKey:key];
} fail:nil];
}
}
});
}
- (void)createTimer {
// 獲得隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 創(chuàng)建一個定時器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設(shè)置開始時間: 1秒之后
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
// 設(shè)置時間間隔
uint64_t interval = (uint64_t)(kSendLocalCacheLogsInterval * NSEC_PER_SEC);
// 設(shè)置定時器
dispatch_source_set_timer(self.timer, start, interval, 0);
// 設(shè)置回調(diào)
__weak __typeof(self)weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
[strongSelf sendLocalCacheLogsToAliyun];
});
//由于定時器默認(rèn)是暫停的所以我們啟動一下
//啟動定時器
dispatch_resume(self.timer);
}
@end
自定義logger
- 從
CocoaLumberjack
的類DDAbstractDatabaseLogger
繼承而來。這個類不能直接使用毁菱,只能繼承膛腐。關(guān)于數(shù)據(jù)庫的存取操作睛约,需要子類覆蓋。
#import <CocoaLumberjack/DDAbstractDatabaseLogger.h>
@interface XXXLogger : DDAbstractDatabaseLogger
@end
- 這種通過“虛基類”的方式來提供功能的方法在
C++
中比較常見哲身,在Object-C
中比較少見
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Override Me
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 每次打log的時候都會進(jìn)來辩涝,一般在這里把log搜集到一個數(shù)組中,等達(dá)到一定條件再統(tǒng)一發(fā)送
- (BOOL)db_log:(DDLogMessage *)logMessage {
// Override me and add your implementation.
//
// Return YES if an item was added to the buffer.
// Return NO if the logMessage was ignored.
return NO;
}
// 等條件滿足勘天,默認(rèn)的條件是日志書達(dá)到500條怔揩,或者,時間間隔超過1分鐘脯丝,這個函數(shù)都會調(diào)用一下
// 這里是將log數(shù)組發(fā)送到阿里云的地方
- (void)db_save {
// Override me and add your implementation.
}
// 下面兩個是關(guān)于本地數(shù)據(jù)庫刪除的實現(xiàn)函數(shù)商膊。我們其實并不需要本地數(shù)據(jù)庫緩存,這兩個函數(shù)可以不實現(xiàn)宠进。
- (void)db_delete {
// Override me and add your implementation.
}
- (void)db_saveAndDelete {
// Override me and add your implementation.
}
- 監(jiān)聽消息
UIApplicationWillResignActiveNotification
晕拆,在程序進(jìn)入后臺之前保存一下當(dāng)前的日志。
#import "XXXLogger.h"
#import "XXXLogSender.h"
#define kLogNumberThreshold 50 // 達(dá)到多少條就保存?zhèn)骱笈_
#define kLogTimeThreshold (5 * 60) // 間隔多少時間(秒)就保存?zhèn)骱笈_
// 數(shù)組容量達(dá)到這個最大值的話材蹬,說明網(wǎng)絡(luò)出了問題
#define kLogMaxCapacity (kLogNumberThreshold * 10)
@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 = kLogNumberThreshold;
self.saveInterval = kLogTimeThreshold;
// 監(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 {
if ([self.logs count] > kLogMaxCapacity) {
// 如果段時間內(nèi)進(jìn)入大量log整吆,并且遲遲發(fā)不到服務(wù)器上,我們可以判斷哪里出了問題辉川,在這之后的log暫時不處理了表蝙。
// 但我們依然要告訴DDLog這個存進(jìn)去了。
return YES;
}
// 對于Debug和verbose級別的日志乓旗,不發(fā)送到后臺
// 但我們依然要告訴 DDLog 這個存進(jìn)去了勇哗。
if ((DDLogLevelDebug == logMessage.flag) || (DDLogLevelVerbose == logMessage.flag)) {
return YES;
}
// 將log放在緩存的數(shù)組中
@synchronized (self) {
// 阿里云要求字典格式的日志;而_logFormatter返回的是字符串寸齐,不符合欲诺;所以這里直接設(shè)置日志的格式
// 自定義的log要傳到阿里云后臺,值處理3級log
NSMutableDictionary *log = [NSMutableDictionary dictionary];
switch (logMessage.flag) {
case DDLogFlagError:
log[kLogKeyLevel] = @"ERROR";
break;
case DDLogFlagWarning:
log[kLogKeyLevel] = @"WARNING";
break;
default:
log[kLogKeyLevel] = @"INFO";
break;
}
log[kLogKeyFileName] = logMessage.fileName;
log[kLogKeyFunction] = logMessage.function;
log[kLogKeyLine] = [NSString stringWithFormat:@"%ld", (unsigned long)logMessage.line];
log[kLogKeyContent] = logMessage.message;
[self.logs addObject:[log copy]];
}
return YES;
}
- (void)db_save {
//如果緩存內(nèi)沒數(shù)據(jù)渺鹦,啥也不做
if (0 == [self.logs count]) {
return;
}
// 將緩存在數(shù)組中的logs傳給后臺扰法,并清空緩存數(shù)組
[XXXLogSender sendLogs:[self.logs copy]];
@synchronized (self) {
[self.logs removeAllObjects];
}
}
// selector
// 手機(jī)退到后臺,不管條件是否滿足毅厚,都保存一次塞颁,也就是向阿里云發(fā)送一次
- (void)onWillResignActive:(NSNotification *)notification {
dispatch_async(self.loggerQueue, ^{
[self db_save];
});
}
@end
實際效果
日志庫
CocoaLumberjack
基本還可以,提前了解坑,功能上根據(jù)實際情況做取舍祠锣,沒有遇到大的問題酷窥,相對比較順利。本地緩存
YYCache
用下來比較順手伴网,沒有遇到坑蓬推,功能滿足要求,接口也比較人性化澡腾,個人比較推薦使用沸伏。這個比自己寫數(shù)據(jù)庫,文件动分,或者序列化什么的都要方便毅糟。阿里云接口
AliyunLogObjc
,感覺比較差澜公。問題主要有以下急個:
(1)接口設(shè)計很差姆另,不好用。格式要求比較復(fù)雜坟乾。
(2)加密算法導(dǎo)致程序崩潰迹辐,雖然情況比較特殊。
(3)還有幾個warning
糊渊,比如(long)
強(qiáng)轉(zhuǎn)之類的,接口少nullable
修飾之類的慧脱,雖然是小問題渺绒,但感覺不好。
(4)Swift
版本提供cathage
是支持的菱鸥,但是Object-C
版本宗兼,CocoaPods
都不提供,感覺有點不合適氮采。
(5)至于Http
還是https
,這個還是我們這里的證書問題殷绍。日志,還是先發(fā)自己的后臺鹊漠,經(jīng)過一輪搜集之后主到,由自己的后臺再轉(zhuǎn)阿里云,這個方案比較好一點躯概。將終端的日志先收集起來是最重要的登钥。至于轉(zhuǎn)阿里云后臺,自己的后臺可以統(tǒng)一控制娶靡。