文章目錄
前言
全量日誌
就是app的運行日誌打印等等。有時候光憑Crash
日誌並不能找到並解決問題,如果有Crash
時App
的日誌輸出,則會事半功倍。
CocoaLumberjack
是OSX
和iOS
平臺優秀的全量日誌
抓取第三方庫。github鏈接
此篇文章更着重於分析其實現以及結構組成。
日誌重定向
我們通過日誌重定向可以進行將控制檯的輸出日誌存儲到文件中
- (void)redirectLogToDocumentFolder
{
// 獲取沙盒路徑
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *documentDirectory = [paths objectAtIndex:0];
// 獲取打印輸出文件路徑
NSString *fileName = [NSString stringWithFormat:@"myData.log"];
NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:fileName];
// 先刪除已經存在的文件
NSFileManager *defaultManager = [NSFileManager defaultManager];
[defaultManager removeItemAtPath:logFilePath error:nil];
// 將NSLog的輸出重定向到文件,因爲C語言的printf打印是往stdout打印的,這裏也把它重定向到文件
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding],"a+", stdout);
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding],"a+", stderr);
}
這樣的壞處是,當重定向之後,控制檯不再打印日誌輸出了,雖然我們可以判斷xcode是否連接,然後再進行重定向,來解決連接xcode調試的問題。
但是還是有不足,就是當你的日誌輸出你只想自己看到,而不想影響控制檯的輸出時,顯然重定向不能做到,我們需要自己的日誌輸出入口,同時還要能監聽到系統的日誌輸出,而不影響控制檯的日誌。
CocoaLumberjack
就可以幫我們實現這個功能。一般來說我們需要創建3個logger
,分別是
ASL
用於記錄系統的日誌輸出,這個輸出xcode有些
不會打印出來TTY
控制檯會打印的日誌輸出File
寫入文件中,存爲log文件,然後上傳,便於分析問題。
Lumberjack組成
傳統的NSLog()
函數將它的輸出指向兩個地方:
- 蘋果系統日誌
ASL (Apple System Logs)
StdErr
(如果StdErr
是一個TTY
),所以日誌語句顯示在Xcode
控制檯
Capture 捕捉
DDASLLogCapture
爲CocoaLumberjack
中唯一的一個Capture
類,用與捕獲ASL
日誌
Logger 輸出
logger
用於輸出日誌,有
DDASLLogger
用於輸出到ASL
DDOSLogger
是iOS 10
之後公開的日誌輸出方式,用於取代ASL
,你可以在官方文檔 中查看接口DDTTYLogger
該類爲終端輸出
或Xcode控制檯
輸出提供一個日誌記錄器DDFileLogger
用於將日誌輸出到文件中,我們一般存儲日誌文件之後進行壓縮。
要實現替換 NSLog()
的功能,您可以簡單地添加DDASLLogger
和一個DDTTYLogger
。
但是,如果您選擇使用文件記錄器(DDFileLogger)
(以獲得更快的性能),
你可以選擇只使用一個文件記錄器(DDFileLogger)
和一個tty記錄器(DDTTYLogger)
。
message and formatter 消息以及格式化
DDLogMessage
是封裝的消息實體
DDLogFormatter
是對輸出的字符串格式化的類別
你可以對照CocoaLumberjack
源碼中的Demos
進行更好的理解
裏面各種場景都很有參考意義。
ASL 日誌系統
ASL (Apple system logger)
是蘋果公司自己實現的一套輸出日誌的接口。
通過DDASLLogger.m
文件,我們瞭解到captureAslLogs
做了捕捉日誌輸出的功能
+ (void)captureAslLogs {
@autoreleasepool
{
/*
We use ASL_KEY_MSG_ID to see each message once, but there's no
obvious way to get the "next" ID. To bootstrap the process, we'll
search by timestamp until we've seen a message.
*/
struct timeval timeval = {
.tv_sec = 0
};
gettimeofday(&timeval, NULL);
unsigned long long startTime = (unsigned long long)timeval.tv_sec;
__block unsigned long long lastSeenID = 0;
/*
syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message)
through the notify API when it saves messages to the ASL database.
There is some coalescing - currently it is sent at most twice per
second - but there is no documented guarantee about this. In any
case, there may be multiple messages per notification.
Notify notifications don't carry any payload, so we need to search
for the messages.
*/
int notifyToken = 0; // Can be used to unregister with notify_cancel().
notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)
{
// At least one message has been posted; build a search query.
@autoreleasepool
{
aslmsg query = asl_new(ASL_TYPE_QUERY);
char stringValue[64];
if (lastSeenID > 0) {
//格式化一個64位的字符串
snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID);
//進行查找 - 按 > seenID 查找
asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC);
} else {
//時間查找
snprintf(stringValue, sizeof stringValue, "%llu", startTime);
asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC);
}
[self configureAslQuery:query];
// Iterate over new messages.
aslmsg msg;
aslresponse response = asl_search(NULL, query);
while ((msg = asl_next(response)))
{
[self aslMessageReceived:msg];
// Keep track of which messages we've seen.
lastSeenID = (unsigned long long)atoll(asl_get(msg, ASL_KEY_MSG_ID));
}
asl_release(response);
asl_free(query);
if (_cancel) {
notify_cancel(token);
return;
}
}
});
}
}
timeval
timeval
表示時間的一個結構體,
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 毫秒 */
};
通過gettimeofday(&timeval, NULL);
能夠獲得當前系統時間
notify_register_dispatch
用於註冊進程間的系統通知,kNotifyASLDBUpdate
是一個通知,當日志消息被添加到ASL
數據庫的時候發出的跨進程通知。
/*
* ASL notifications
* Sent by syslogd to advise clients that new log messages have been
* added to the ASL database.
*/
#define kNotifyASLDBUpdate "com.apple.system.logger.message"
通過/usr/include/notify_keys.h
文件可以查看更多相關通知內容。
aslMessageReceived
入參是aslmsg
類型,aslget
將其轉爲char字符串類型後,再轉爲NSString
NSString *message = @(messageCString);
//這裏獲取秒和毫微秒(十億分之一秒)
const char* secondsCString = asl_get( msg, ASL_KEY_TIME );
const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC );
NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970;
double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0;
//1e9 = 1000000000
NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9);
NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds];
//生成message
DDLogMessage *logMessage = [[DDLogMessage alloc]initWithMessage:message
level:_captureLevel
flag:flag
context:0
file:@"DDASLLogCapture"
function:nil
line:0
tag:nil
options:0
timestamp:timeStamp];
//記錄到文件
[DDLog log:async message:logMessage];
這裏跟蹤log:message:
方法最終也是通過寫入日誌到文件句柄
方式
NSFileHandle *handle = [self lt_currentLogFileHandle];
[handle seekToEndOfFile];
[handle writeData:data];
瞭解到其實現之後,我們就可以自己編寫簡單的ASL
日誌捕獲工具類,或者使用DDASLLogger
輸出到ASL
,DDASLLogger
能夠將DDLogMessage
轉化爲aslmsg
進行賦值之後輸出到ASL
系統。
TTY 控制檯輸出
前面我們說到了日誌重定向,目的是將本來寫入到控制檯的輸出,轉而寫入到文件中。
NSLog
其實就是寫入到文件syslog
中,既然要往文件中寫,那麼肯定就有文件的句柄了,C語言中,有3個句柄,也是我們進行重定向時用到的。
#define stdin __stdinp
#define stdout __stdoutp
#define stderr __stderrp
在iOS平臺中,有以下3個:
#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */
NSLog
是在向STDERR_FILENO
中寫入,你可以使用c語言
的輸出到文件的fprintf
來驗證一下:
NSLog(@"ViewController viewDidLoad");
fprintf (stderr, "%s\n", "ViewController viewDidLoad222");
控制檯可見輸出爲:
2016-06-15 12:57:17.286 TestNSlog[68073:1441419] ViewController viewDidLoad
ViewController viewDidLoad222
關於重定向的更多內容你可以查看 這篇博文
好了,這下說說TTY
的實現,要想實現控制檯的輸出,那麼就輸出到STDERR_FILENO
就行了,源碼中也是這樣的實現,值得注意的是,控制檯的輸出可以輸出顏色,所以DDTTYLogger
實現中包含了很多顏色的處理,你以搭配CLIColor
來了解。
下面是TTYLogger
的輸出部分:
// Write the log message to STDERR
if (isFormatted) {
// The log message has already been formatted.
int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4;
struct iovec v[iovec_len];
if (colorProfile) {
v[0].iov_base = colorProfile->fgCode;
v[0].iov_len = colorProfile->fgCodeLen;
v[1].iov_base = colorProfile->bgCode;
v[1].iov_len = colorProfile->bgCodeLen;
v[iovec_len - 1].iov_base = colorProfile->resetCode;
v[iovec_len - 1].iov_len = colorProfile->resetCodeLen;
} else {
v[0].iov_base = "";
v[0].iov_len = 0;
v[1].iov_base = "";
v[1].iov_len = 0;
v[iovec_len - 1].iov_base = "";
v[iovec_len - 1].iov_len = 0;
}
v[2].iov_base = (char *)msg;
v[2].iov_len = msgLen;
if (iovec_len == 5) {
v[3].iov_base = "\n";
v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1;
}
writev(STDERR_FILENO, v, iovec_len);
} else {
// The log message is unformatted, so apply standard NSLog style formatting.
...
}
os_log 新日誌系統
iOS 10
之後的os_log
更爲簡單
常用接口有:
os_log_with_type
將特定日誌級別的消息(如default、info、debug、error)發送到日誌系統。os_log_debug
向日志系統發送調試級別消息。os_log_info
向日志系統發送信息級消息。os_log_error
向日志系統發送錯誤級消息。os_log_fault
向日志系統發送默認級別的消息。os_log
向日志系統發送一個默認級別的消息。與os_log_fault
一樣
你可以看看 官方文檔
- (void)logMessage:(DDLogMessage *)logMessage {
// Skip captured log messages
if ([logMessage->_fileName isEqualToString:@"DDASLLogCapture"]) {
return;
}
if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) {
NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message;
if (message != nil) {
const char *msg = [message UTF8String];
__auto_type logger = [self logger];
switch (logMessage->_flag) {
case DDLogFlagError :
os_log_error(logger, "%{public}s", msg);
break;
case DDLogFlagWarning:
case DDLogFlagInfo :
os_log_info(logger, "%{public}s", msg);
break;
case DDLogFlagDebug :
case DDLogFlagVerbose:
default :
os_log_debug(logger, "%{public}s", msg);
break;
}
}
}
}
file logger 文件輸出
文件logger
在CocoaLumberjack
算是比較重要的部分了,其實現全部在DDFileLogger
中
DDLogFileManager 文件輸出協議
對於文件管理協議DDLogFileManager
,主要是下面的4個屬性
-
maximumNumberOfLogFiles
要保存在磁盤上的歸檔日誌文件的最大數量。如果這個屬性設置爲3
,將只保留3
個歸檔日誌文件(加上當前活動的日誌文件)在磁盤上,你可以設置0
將其禁用 -
logFilesDiskQuota
日誌佔用的最大空間。在滾動日誌文件時,所有超過logFilesDiskQuota
的舊日誌文件都將被刪除。你可以設置0
將其禁用 -
logsDirectory
所有日誌文件都放在logsDirectory
中。
如果沒有指定特定的logsDirectory
,則使用默認目錄。
-
在
Mac
上,這是在~/Library/Logs/<Application Name>
-
在
iPhone
上,這是在~/Library/Caches/Logs
.日誌文件被命名爲
<bundle identifier> <date> <time>.log
,例如 Example:com.organization.myapp 2013-12-03 17-14.log
存檔的日誌文件
會根據“maximumNumberOfLogFiles”
屬性自動刪除。
-
maximumFileSize
允許日誌文件增長的最大大小(以字節爲單位)。 如果日誌文件大於這個值, 將會生成一個新的日誌文件進行繼續寫入。 -
rollingFrequency
滾日誌文件的頻率。 頻率以NSTimeInterval
的形式給出,它是一個雙精度浮點數,指定以秒爲單位的間隔。 一旦日誌文件變得這麼舊,它就會被重新生成。例如10min = 60x10
就重新生成一個日誌文件
您可以通過將“maximumFileSize
”設置爲0來選擇性地禁用由於文件大小而導致的滾動。 如果你這樣做,滾動是完全基於“rollingFrequency
”。
您可以選擇通過將“rollingFrequency
”設置爲0(或任何非正數)來禁用由於時間而導致的滾動。 如果你這樣做了,滾動僅僅是基於“maximumFileSize
”。
如果您同時禁用“maximumFileSize
”和“rollingFrequency
”,那麼日誌文件將永遠不會被滾動。 這是強烈不鼓勵的。
這些值默認值爲
// maximumFileSize -> kDDDefaultLogMaxFileSize
// rollingFrequency -> kDDDefaultLogRollingFrequency
// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles
// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota
unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024; // 1 MB
NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours
NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files
unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB
文件壓縮
可以參考Demos
中的LogFileCompressor
,使用zlib
壓縮成了gz
壓縮文件。
具體你可以查看CompressingLogFileManager.m
的實現。