既往不戀,縱情向前
一、NSLog概述
1、NSLog是什麼
- NSLog是一個C函式,函式宣告如下:
//Logs an error message to the Apple System Log facility.
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
複製程式碼
-
根據蘋果的文件介紹,NSLog的作用是輸出資訊到標準的Error控制檯和 蘋果的日誌系統(ASL,Apple System Log)裡面(iOS 10之前)。
-
iOS10之後,蘋果使用新的統一日誌系統(Unified Logging System)來記錄日誌,全面取代ASL的方式,此種方式,是把日誌集中存放在記憶體和資料庫裡,並提供單一、高效和高效能的介面去獲取系統所有級別的訊息傳遞。
-
新的統一日誌系統沒有ASL那樣的介面可以讓我們取出全部日誌。
2、NSLog日常使用
-
NSLog在除錯階段,日誌會輸出到到Xcode中,而在iOS真機上,它會輸出到系統的
/var/log/syslog
這個檔案中。 -
在日常開發中,很多人喜歡使用NSLog來輸出除錯資訊,但是都知道NSLog是比較消耗效能呢,NSLog輸出的內容或次數多了之後,甚至會影響App的體驗。
-
於是乎,比較常見的手段是,線上不使用NSLog,DEBUG下才真正使用NSLog。
#if DEBUG
#define MYLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
#else
#define MYLOG(fmt,...) {}
#endif
複製程式碼
3、常見的日記收集框架
- 日誌收集主要用了兩個開源框架來實現:PLCrashReporter與CocoaLumberjack。PLCrashReporter主要用來崩潰日誌收集,CocoaLumberjack是用來收集非崩潰日誌。
- CocoaLumberjack中實現了對NSLog日誌的捕獲。
4、捕獲NSLog日誌有三種方式
- iOS 10以前可以通過ASL介面來獲取
- 通過fishhook庫hook NSLog方法重定向NSLog函式
- 使用dup2函式和STDERR控制程式碼重定向NSLog函式
二、獲取NSLog的日誌輸出(iOS 10前)
參考CocoaLumberjack中的DDASLLogCapture實現
1、流程介紹
- 執行DDASLLogCapture的start方法,啟動一個非同步全域性佇列去捕獲ASL儲存的日誌;
+ (void)start {
//...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
[self captureAslLogs];
});
}
複製程式碼
- 當日志被儲存到ASL的資料庫時候,syslogd(系統裡用於接收分發日誌訊息的日誌守護程式)會發出一條通知。因為發過來的這一條通知可能有多條日誌,需要先將幾條日誌進行合併。
+ (void)captureAslLogs {
//....
}
複製程式碼
- 將獲得到的資料轉成char 字串型別,再轉成NSString型別,最後封裝成DDLogMessage物件,通過
[DDLog log: message:]
方法將日誌記錄下來。
+ (void)aslMessageReceived:(aslmsg)msg {
//...
}
複製程式碼
說明:以上方法不會影響Xcode控制檯的輸出,無侵入。
2、註冊程式間的系統通知
captureAslLogs
中通過notify_register_dispatch
來註冊監聽程式間的系統通知;
notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)
{
//...
});
複製程式碼
- 其中巨集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"
複製程式碼
- 將日誌儲存到ASL資料庫時還有很多通知,比如巨集kNotifyVFSLowDiskSpace表示:系統磁碟空間不足,捕獲到這個通知時,可以去清理快取空間,避免快取寫入磁碟失敗的情況。
#define kNotifyVFSLowDiskSpace "com.apple.system.lowdiskspace"
複製程式碼
三、NSLog重定向
1、介紹
- 在iOS10之後,新的統一日誌系統(Unified Logging System)全面取代ASL,沒有ASL那樣的介面可以讓我們取出全部日誌,所以為了相容新的統一日誌系統,你就需要對NSLog日誌的輸出進行重定向。
- NSLog 進行重定向,可以採用 Hook的方式。因為 NSLog 本身就是一個 C 函式,可以使用fishhook進行重定向。
- fishhook是Facebook提供的一個動態修改連結Mach-O檔案的工具,能夠hook C函式。
2、fishhook原理
-
APP執行時,Mach-O檔案被dyld(動態載入器)載入進記憶體
-
ASLR(地址空間佈局隨機化)讓Mach-O被載入時記憶體地址隨機分配
-
蘋果的PIC位置與程式碼獨立技術,讓Mach-O呼叫系統庫函式時,先在Mach-O表中的
_DATA段
建立一個指標指向外部庫函式,dyld載入MachO時知道外部庫函式的呼叫地址,會動態的把_DATA段的指標指向外部庫函式 -
fishhook能夠替換NSLog等庫函式,這事是因為Mach-O的符號表裡有NSLog等,可以通過符號表找到NSLog字串。
說明:具體原理參考iOS逆向工程 - fishhook原理
3、利用fishhook hook NSLog函式
實現程式碼如下:
//申明一個函式指標用於儲存原NSLog的真實函式地址
static void (*orig_nslog)(NSString *format, ...);
//NSLog重定向
void redirect_nslog(NSString *format, ...) {
//可以新增自己的處理,比如輸出到自己的持久化儲存系統中
//繼續執行原來的 NSLog
va_list va;
format = [NSString stringWithFormat:@"[hook success]%@",format];
va_start(va, format);
NSLogv(format, va);
va_end(va);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};
rebind_symbols((struct rebinding[1]){nslog_rebinding}, 1);
NSLog(@"%@, hello word!",@"ss");
}
return
}
//[hook success]ss, hello word!
複製程式碼
- 利用fishhook對方法的符號地址進行了重新板頂,從而只要是NDSLog的呼叫就會轉向redirect_nslog方法呼叫。
四、dup2重定向
1、介紹
- NSLog最後重定向的控制程式碼是STDERR,NSLog輸出的日誌內容,最終都通過STDERR控制程式碼來記錄,而dup2函式式專門進行檔案重定向的;
- 可以使用dup2重定向STDERR控制程式碼,將內容重定向指定的位置,如寫入檔案,上傳伺服器,顯示到View上。
2、核心程式碼
- 實現重定向,需要通過
NSPipe
建立一個管道,pipe有讀端和寫端,然後通過dup2
將標準輸入重定向到pipe的寫端。再通過NSFileHandle
監聽pipe的讀端,最後再處理讀出的資訊。 - 之後通過printf或者NSLog寫資料,都會寫到pipe的寫端,同時pipe會將這些資料直接傳送到讀端,最後通過NSFileHandle的監控函式取出這些資料。
- (void)redirectSTD:(int )fd {
NSPipe * pipe = [NSPipe pipe] ;
NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;
int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
dup2(pipeFileHandle, fd) ;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(redirectNotificationHandle:)
name:NSFileHandleReadCompletionNotification
object:pipeReadHandle] ;
[pipeReadHandle readInBackgroundAndNotify];
}
- (void)redirectNotificationHandle:(NSNotification *)nf {
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
//可以新增自己的處理,可以將內容顯示到View,或者是存放到另一個檔案中等等
//todo
[[nf object] readInBackgroundAndNotify];
}
//使用
[self redirectSTD:STDERR_FILENO];
複製程式碼