一次日志庫的導(dǎo)入經(jīng)歷

基本的方案

接口函數(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模式還是其他什么模式,DDLogLevelVerboseDDLogLevelDebug兩種級別的日志都不發(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)用YYCacheAPI進(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)一控制娶靡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牧牢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌塔鳍,老刑警劉巖伯铣,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異轮纫,居然都是意外死亡腔寡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門蜡感,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹬蚁,“玉大人,你說我怎么就攤上這事郑兴∠” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵情连,是天一觀的道長叽粹。 經(jīng)常有香客問我,道長却舀,這世上最難降的妖魔是什么虫几? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮挽拔,結(jié)果婚禮上辆脸,老公的妹妹穿的比我還像新娘。我一直安慰自己螃诅,他們只是感情好啡氢,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著术裸,像睡著了一般倘是。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袭艺,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天搀崭,我揣著相機(jī)與錄音,去河邊找鬼猾编。 笑死瘤睹,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的答倡。 我是一名探鬼主播默蚌,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼苇羡!你這毒婦竟也來了绸吸?” 一聲冷哼從身側(cè)響起鼻弧,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锦茁,沒想到半個月后攘轩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡码俩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年度帮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稿存。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡笨篷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瓣履,到底是詐尸還是另有隱情率翅,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布袖迎,位于F島的核電站冕臭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏燕锥。R本人自食惡果不足惜辜贵,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望归形。 院中可真熱鬧托慨,春花似錦、人聲如沸暇榴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跺撼。三九已至窟感,卻和暖如春讨彼,著一層夾襖步出監(jiān)牢的瞬間歉井,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工哈误, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留哩至,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓蜜自,卻偏偏與公主長得像菩貌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子重荠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫箭阶、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,098評論 4 62
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,099評論 25 707
  • 一直以來感覺都太在乎而不敢靠近,從來不敢對他說喜歡他仇参,他是我的后桌嘹叫,我們雖然高一時候就在一個班,但是一直都沒有說...
    白干安喵閱讀 277評論 0 0
  • 孫悟空神功蓋世,最讓人羨慕的怕磨,莫過于他一身用之不盡喂饥、拔之不竭的猴毛。危急時肠鲫,變成一個同貌的假身迷惑眾生员帮,而真身去做...
    不町閱讀 263評論 1 1
  • 上次寫了一篇關(guān)于標(biāo)簽的文章集侯,這次寫一下穿衣那點事。 現(xiàn)代年輕一代都喜歡追潮流帜消,主流喜歡什么花樣就去買棠枉;今年流行什么...
    一鈅I鈅一閱讀 714評論 0 7