利用 CocoaLumberjack 搭建自己的 Log 系统
2019獨角獸企業重金招聘Python工程師標準>>>
先說下需求,我理想中的 Log 系統需要:
可以設定 Log 等級
可以積攢到一定量的 log 后,一次性發送給服務器,絕對不能打一個 Log 就發一次
可以一定時間后,將未發送的 log 發送到服務器
可以在 App 切入后臺時將未發送的 log 發送到服務器
其他一些需求,比如可以遠程設定發送 log 的等級閥值,還有閥值的有效期等,和本文無關就不寫了。
開始動手前,先了解下?CocoaLumberjack?是什么:
CocoaLumberjack?最早是由?Robbie Hanson?開發的日志庫,可以在 iOS 和 MacOSX 開發上使用。其簡單,快讀,強大又不失靈活。它自帶了幾種log方式,分別是:
DDASLLogger 將 log 發送給蘋果服務器,之后在 Console.app 中可以查看
DDTTYLogger 將 log 發送給 Xcode 的控制臺
DDFileLogger 講 log 寫入本地文件
CocoaLumberjack?打一個 log 的流程大概就是這樣的:
所有的 log 都會發給 DDLog 對象,其運行在自己的一個GCD隊列(GlobalLoggingQueue),之后,DDLog 會將 log 分發給其下注冊的一個或多個 Logger,這步在多核下是并發的,效率很高。每個 Logger 處理收到的 log 也是在它們自己的 GCD隊列下(loggingQueue)做的,它們詢問其下的 Formatter,獲取 Log 消息格式,然后最終根據 Logger 的邏輯,將 log 消息分發到不同的地方。
因為一個 DDLog 可以把 log 分發到所有其下注冊的 Logger 下,也就是說一個 log 可以同時打到控制臺,打到遠程服務器,打到本地文件,相當靈活。
CocoaLumberjack?支持 Log 等級:
typedef?NS_OPTIONS(NSUInteger,?DDLogFlag)?{DDLogFlagError??????=?(1?<<?0),?//?0...00001DDLogFlagWarning????=?(1?<<?1),?//?0...00010DDLogFlagInfo???????=?(1?<<?2),?//?0...00100DDLogFlagDebug??????=?(1?<<?3),?//?0...01000DDLogFlagVerbose????=?(1?<<?4)??//?0...10000};typedef?NS_ENUM(NSUInteger,?DDLogLevel)?{DDLogLevelOff???????=?0,DDLogLevelError?????=?(DDLogFlagError),???????????????????????//?0...00001DDLogLevelWarning???=?(DDLogLevelError???|?DDLogFlagWarning),?//?0...00011DDLogLevelInfo??????=?(DDLogLevelWarning?|?DDLogFlagInfo),????//?0...00111DDLogLevelDebug?????=?(DDLogLevelInfo????|?DDLogFlagDebug),???//?0...01111DDLogLevelVerbose???=?(DDLogLevelDebug???|?DDLogFlagVerbose),?//?0...11111DDLogLevelAll???????=?NSUIntegerMax???????????????????????????//?1111....11111?(DDLogLevelVerbose?plus?any?other?flags)};DDLogLevel 定義了全局的 log 等級,DDLogFlag 是我們打 log 時設定的 log 等級,CocoaLumberjack 會比較兩者,如果 flag 低于 level,則不會打 log:
#define?LOG_MAYBE(async,?lvl,?flg,?ctx,?tag,?fnct,?frmt,?...)?\????????do?{?if(lvl?&?flg)?LOG_MACRO(async,?lvl,?flg,?ctx,?tag,?fnct,?frmt,?##__VA_ARGS__);?}?while(0)DDLogger 協議定義了 logger 對象需要遵從的方法和變量,為了方便使用,其提供了 DDAbstractLogger 對象,我們只需要繼承該對象就可以自定義自己的 logger。對于第二點和第三點需求,我們可以利用 DDAbstractDatabaseLogger,其也是繼承自 DDAbstractLogger,并在其上定義了 saveThreshold, saveInterval 等控制參數。這個 logger 本身是針對寫入數據庫的 log 設計的,我們也可以利用它這幾個參數,實現我們上面所提的需求的第二和第三點。
對于第二點,設定 _saveThreshold 值即可,比如如果希望積攢1000條 log 再一次性發送,就賦值 1000.
對于第三點,設定 _saveInterval,比如如果希望每分鐘發送一次,就設定 60.
由此,CocoaLumberjack?已經實現了需求中的 1、2、3 點,我們要做的無非是自定義 Logger 和 Formatter,將 log 的最終去處改為發送到我們自己的服務器中。
而第四點,我們可以監聽 UIApplicationWillResignActiveNotification 事件,當觸發時,手動調用 logger 的 db_save 方法,發送數據給服務器。
廢話了半天,現在看下實現。
首先我們設定 log 的消息結構。自定義一個 LogFormatter, 遵從 DDLogFormatter 協議,我們需要重寫 formatLogMessage 這個方法,這個方法返回值是 NSString,就是最終 log 的消息體字符串。而輸入參數 logMessage 是由 logger 發的一個 DDLogMessage 對象,包含了一些必要的信息:
@interface?DDLogMessage?:?NSObject?<NSCopying>{//?Direct?accessors?to?be?used?only?for?performance@publicNSString?*_message;DDLogLevel?_level;DDLogFlag?_flag;NSUInteger?_context;NSString?*_file;NSString?*_fileName;NSString?*_function;NSUInteger?_line;id?_tag;DDLogMessageOptions?_options;NSDate?*_timestamp;NSString?*_threadID;NSString?*_threadName;NSString?*_queueLabel;}可以利用這些信息構建自己的 log 消息體。比如我們這里只需要 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";//這里的格式:?{"location":"myfile.m:120(void?a::sub(int)"},?文件名,行數和函數名是用的編譯器宏?__FILE__,?__LINE__,?__PRETTY_FUNCTION__logDict[@"location"]?=?[NSString?stringWithFormat:@"%@:%lu(%@)",?locationString,?(unsigned?long)logMessage->_line,?logMessage->_function]//嘗試將logDict內容轉為字符串,其實這里可以直接構造字符串,但真實項目中,肯定需要很多其他的信息,不可能僅僅文件名、行數和函數名就夠了的。NSError?*error;NSData?*outputJson?=?[NSJSONSerialization?dataWithJSONObject:logfields?options:0?error:&error];if?(error)return?@"{\"location\":\"error\"}"NSString?*jsonString?=?[[NSString?alloc]?initWithData:outputJson?encoding:NSUTF8StringEncoding];if?(jsonString)return?jsonString;return?@"{\"location\":\"error\"}"}接下來自定義 logger,其繼承自 DDAbstractDatabaseLogger。在初始化方法中,先設定好一些參數,以及添加一個UIApplicationWillResignActiveNotification的觀察者,用以實現第四個需求。
-?(instancetype)init?{self?=?[super?init];if?(self)?{self.deleteInterval?=?0;self.maxAge?=?0;self.deleteOnEverySave?=?NO;self.saveInterval?=?60;self.saveThreshold?=?500;//別忘了在?dealloc?里?removeObserver[[NSNotificationCenter?defaultCenter]?addObserver:selfselector:@selector(saveOnSuspend)name:@"UIApplicationWillResignActiveNotification"object:nil];}return?self;}-?(void)saveOnSuspend?{dispatch_async(_loggerQueue,?^{[self?db_save];});}每次打 log 時,db_log: 會被調用,我們在這個函數里,將 log 發給 formatter,將返回的 log 消息體字符串保存在緩沖中。 db_log 的返回值告訴 DDLog 該條 log 是否成功保存進緩存。
-?(BOOL)db_log:(DDLogMessage?*)logMessage{if?(!_logFormatter)?{//沒有指定?formatterreturn?NO;}if?(!_logMessagesArray)_logMessagesArray?=?[NSMutableArray?arrayWithCapacity:500];?//?我們的saveThreshold只有500,所以一般情況下夠了if?([_logMessagesArray?count]?>?2000)?{//?如果段時間內進入大量log,并且遲遲發不到服務器上,我們可以判斷哪里出了問題,在這之后的?log?暫時不處理了。//?但我們依然要告訴?DDLog?這個存進去了。return?YES;}//利用?formatter?得到消息字符串,添加到緩存[_logMessagesArray?addObject:[_logFormatter?formatLogMessage:logMessage]];return?YES;}當1分鐘或者未寫入 log 數達到 500 時, db_save 就會被調用,我們在這里,將緩存的數據上傳到自己的服務器。
-?(void)db_save{//判斷是否在?logger?自己的GCD隊列中if?(![self?isOnInternalLoggerQueue])NSAssert(NO,?@"db_saveAndDelete?should?only?be?executed?on?the?internalLoggerQueue?thread,?if?you're?seeing?this,?your?doing?it?wrong.");//如果緩存內沒數據,啥也不做if?([_logMessagesArray?count]?==?0)return;獲取緩存中所有數據,之后將緩存清空NSArray?*oldLogMessagesArray?=?[_logMessagesArray?copy];_logMessagesArray?=?[NSMutableArray?arrayWithCapacity:0];//用換行符,把所有的數據拼成一個大字符串?NSString?*logMessagesString?=?[oldLogMessagesArray?componentsJoinedByString:@"\n"];//發送給咱自己服務器(自己實現了)[self?post:logMessagesString];}最后,我們需要在程序某處定義全局 log 等級(我這里使用 Info),并在 AppDelegate 的 didFinishLaunchingWithOptions 里初始化所有 Log 相關的東西:
static?NSUInteger?LOG_LEVEL_DEF?=?DDLogLevelInfo;-?(BOOL)application:(UIApplication?*)application?didFinishLaunchingWithOptions:(NSDictionary?*)launchOptions{MyLogger?*logger?=?[MyLogger?new];[logger?setLogFormatter:[MyLogFormatter?new]];[DDLog?addLogger:logger];//....}然后就可以利用 DDLogError, DDLogWarning 等宏在程序中打 log 了。使用方法與 NSLog 一樣。這幾個宏的定義:
//注意,DDLogError?是肯定同步的#define?DDLogError(frmt,?...)?LOG_MAYBE(NO,?LOG_LEVEL_DEF,?DDLogFlagError,?0,?nil,?__PRETTY_FUNCTION__,?frmt,?##__VA_ARGS__)#define?DDLogWarn(frmt,?...)?LOG_MAYBE(LOG_ASYNC_ENABLED,?LOG_LEVEL_DEF,?DDLogFlagWarning,?0,?nil,?__PRETTY_FUNCTION__,?frmt,?##__VA_ARGS__)#define?DDLogInfo(frmt,?...)?LOG_MAYBE(LOG_ASYNC_ENABLED,?LOG_LEVEL_DEF,?DDLogFlagInfo,?0,?nil,?__PRETTY_FUNCTION__,?frmt,?##__VA_ARGS__)#define?DDLogDebug(frmt,?...)?LOG_MAYBE(LOG_ASYNC_ENABLED,?LOG_LEVEL_DEF,?DDLogFlagDebug,?0,?nil,?__PRETTY_FUNCTION__,?frmt,?##__VA_ARGS__)#define?DDLogVerbose(frmt,?...)?LOG_MAYBE(LOG_ASYNC_ENABLED,?LOG_LEVEL_DEF,?DDLogFlagVerbose,?0,?nil,?__PRETTY_FUNCTION__,?frmt,?##__VA_ARGS__)最后感謝?CocoaLumberjack?的作者?Robbie Hanson?,如果你喜歡他開發的庫,比如?XMPPFramework,別忘了幫他買杯啤酒哦~
轉載于:https://my.oschina.net/u/1440723/blog/381812
總結
以上是生活随笔為你收集整理的利用 CocoaLumberjack 搭建自己的 Log 系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5W2H方法:七问分析法
- 下一篇: IOS 技术与面试