iOS中日志同步獲取NSLog重定向以及其他詳解
前言
對(duì)于那些做后端開(kāi)發(fā)的工程師來(lái)說(shuō),看LOG解Bug應(yīng)該是理所當(dāng)然的事,但我接觸到的移動(dòng)應(yīng)用開(kāi)發(fā)的工程師里面,很多人并沒(méi)有這個(gè)意識(shí),查Bug時(shí)總是一遍一遍的試圖重現(xiàn),試圖調(diào)試,特別是對(duì)一些不太容易重現(xiàn)的Bug經(jīng)常焦頭爛額。
我們?cè)谡鏅C(jī)測(cè)試時(shí)經(jīng)常會(huì)發(fā)現(xiàn)一個(gè)難題是無(wú)法查看真機(jī)的NSLog類(lèi)型的實(shí)時(shí)日志,這時(shí)候需要RD復(fù)現(xiàn)問(wèn)題來(lái)定位當(dāng)時(shí)的日志,以方便查找問(wèn)題。這個(gè)問(wèn)題在測(cè)試中是非常常見(jiàn)的,也是功能測(cè)試會(huì)花費(fèi)比較長(zhǎng)時(shí)間的一個(gè)原因。
以下我們討論下能即時(shí)查看日志的幾種方案。
NSLog輸出到哪里?
在iOS開(kāi)發(fā)中,我們經(jīng)常會(huì)用到NSLog調(diào)試,但是我們卻不太了解它。在NSLog本質(zhì)是一個(gè)C函數(shù),它的函數(shù)聲明如下:
FOUNDATION_EXPORT void NSLog(NSString *format, ...)
系統(tǒng)對(duì)它的說(shuō)明是:Logs an error message to the Apple System Log facility.。他是用來(lái)輸出信息到標(biāo)準(zhǔn)Error控制臺(tái)上去的,其內(nèi)部其實(shí)是使用Apple System Log的API。在調(diào)試階段,日志會(huì)輸出到到Xcode中,而在iOS真機(jī)上,它會(huì)輸出到系統(tǒng)的/var/log/syslog這個(gè)文件中。
在iOS中,把日志輸出到文件中的句柄在unistd.h文件中有定義:
#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上,我們可以在iOS中使用c語(yǔ)言輸出到的文件的fprintf來(lái)驗(yàn)證:
NSLog(@"iOS NSLog"); fprintf (stderr, "%s\n", "fprintf log");
由于fprintf并不會(huì)像NSLog那樣,在內(nèi)部調(diào)用ASL接口,所以只是單純的輸出信息,并沒(méi)有添加日期、進(jìn)程名、進(jìn)程id等,也不會(huì)自動(dòng)換行。
ASL讀取日志
首先我們可以想到的是既然日志寫(xiě)入系統(tǒng)的syslog中,那我們可以直接讀取這些日志。從ASL讀取日志的核心代碼如下:
#import <asl.h>
// 從日志的對(duì)象aslmsg中獲取我們需要的數(shù)據(jù)
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage{
SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
if (timestamp) {
NSTimeInterval timeInterval = [@(timestamp) integerValue];
const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
if (nanoseconds) {
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
}
logMessage.timeInterval = timeInterval;
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
}
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
if (sender) {
logMessage.sender = @(sender);
}
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
if (messageText) {
logMessage.messageText = @(messageText);//NSLog寫(xiě)入的文本內(nèi)容
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
logMessage.messageID = [@(messageID) longLongValue];
}
return logMessage;
}
+ (NSMutableArray<SystemLogMessage *> *)allLogMessagesForCurrentProcess{
asl_object_t query = asl_new(ASL_TYPE_QUERY);
// Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
aslresponse response = asl_search(NULL, query);
aslmsg aslMessage = NULL;
NSMutableArray *logMessages = [NSMutableArray array];
while ((aslMessage = asl_next(response))) {
[logMessages addObject:[SystemLogMessage logMessageFromASLMessage:aslMessage]];
}
asl_release(response);
return logMessages;
}
使用以上方法的好處是不會(huì)影響Xcode控制臺(tái)的輸出,可以用非侵入性的方式來(lái)讀取日志。
NSLog重定向
另一種方式就是重定向NSLog,這樣NSLog就不會(huì)寫(xiě)到系統(tǒng)的syslog中了。
dup2重定向
通過(guò)重定向,可以直接截取stdout,stderr等標(biāo)準(zhǔn)輸出的信息,然后保存在想要存儲(chǔ)的位置,上傳到服務(wù)器或者顯示到View上。
要做到重定向,需要通過(guò)NSPipe創(chuàng)建一個(gè)管道,pipe有讀端和寫(xiě)端,然后通過(guò)dup2將標(biāo)準(zhǔn)輸入重定向到pipe的寫(xiě)端。再通過(guò)NSFileHandle監(jiān)聽(tīng)pipe的讀端,最后再處理讀出的信息。
之后通過(guò)printf或者NSLog寫(xiě)數(shù)據(jù),都會(huì)寫(xiě)到pipe的寫(xiě)端,同時(shí)pipe會(huì)將這些數(shù)據(jù)直接傳送到讀端,最后通過(guò)NSFileHandle的監(jiān)控函數(shù)取出這些數(shù)據(jù)。
核心代碼如下:
- (void)redirectStandardOutput{
//記錄標(biāo)準(zhǔn)輸出及錯(cuò)誤流原始文件描述符
self.outFd = dup(STDOUT_FILENO);
self.errFd = dup(STDERR_FILENO);
#if BETA_BUILD
stdout->_flags = 10;
NSPipe *outPipe = [NSPipe pipe];
NSFileHandle *pipeOutHandle = [outPipe fileHandleForReading];
dup2([[outPipe fileHandleForWriting] fileDescriptor], STDOUT_FILENO);
[pipeOutHandle readInBackgroundAndNotify];
stderr->_flags = 10;
NSPipe *errPipe = [NSPipe pipe];
NSFileHandle *pipeErrHandle = [errPipe fileHandleForReading];
dup2([[errPipe fileHandleForWriting] fileDescriptor], STDERR_FILENO);
[pipeErrHandle readInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectOutNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeOutHandle];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(redirectErrNotificationHandle:) name:NSFileHandleReadCompletionNotification object:pipeErrHandle];
#endif
}
-(void)recoverStandardOutput{
#if BETA_BUILD
dup2(self.outFd, STDOUT_FILENO);
dup2(self.errFd, STDERR_FILENO);
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
}
// 重定向之后的NSLog輸出
- (void)redirectOutNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// YOUR CODE HERE... 保存日志并上傳或展示
#endif
[[nf object] readInBackgroundAndNotify];
}
// 重定向之后的錯(cuò)誤輸出
- (void)redirectErrNotificationHandle:(NSNotification *)nf{
#if BETA_BUILD
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// YOUR CODE HERE... 保存日志并上傳或展示
#endif
[[nf object] readInBackgroundAndNotify];
}
文件重定向
另一種重定向的方式是利用c語(yǔ)言的freopen函數(shù)進(jìn)行重定向,將寫(xiě)往stderr的內(nèi)容重定向到我們制定的文件中去,一旦執(zhí)行了上述代碼那么在這個(gè)之后的NSLog將不會(huì)在控制臺(tái)顯示了,會(huì)直接輸出在指定的文件中。
在模擬器中,我們可以使用終端的tail命令(tail -f xxx.log)對(duì)這個(gè)文件進(jìn)行實(shí)時(shí)查看,就如同我們?cè)赬code的輸出窗口中看到的那樣,你還可以結(jié)合grep命令進(jìn)行實(shí)時(shí)過(guò)濾查看,非常方便在大量的日志信息中迅速定位到我們要的日志信息。
FILE * freopen ( const char * filename, const char * mode, FILE * stream );
具體代碼如下:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = [paths objectAtIndex:0]; NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/xxx.log"]; //redirect NSLog freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
這樣我們就可以把可獲取的日志文件發(fā)送給服務(wù)端或者通過(guò)itunes共享出來(lái)。但是由于iOS嚴(yán)格的沙盒機(jī)制,我們無(wú)法知道stderr原來(lái)的文件路徑,也無(wú)法直接使用沙盒外的文件,所以freopen無(wú)法重定向回去,只能使用第1點(diǎn)所述的dup和dup2來(lái)實(shí)現(xiàn)。
// 重定向 int origin1 = dup(STDERR_FILENO); FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr); // 恢復(fù)重定向 dup2(origin1, STDERR_FILENO);
使用GCD的dispatch Source重定向方式
具體代碼如下:
- (dispatch_source_t)_startCapturingWritingToFD:(int)fd {
int fildes[2];
pipe(fildes); // [0] is read end of pipe while [1] is write end
dup2(fildes[1], fd); // Duplicate write end of pipe "onto" fd (this closes fd)
close(fildes[1]); // Close original write end of pipe
fd = fildes[0]; // We can now monitor the read end of the pipe
char* buffer = malloc(1024);
NSMutableData* data = [[NSMutableData alloc] init];
fcntl(fd, F_SETFL, O_NONBLOCK);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_cancel_handler(source, ^{
free(buffer);
});
dispatch_source_set_event_handler(source, ^{
@autoreleasepool {
while (1) {
ssize_t size = read(fd, buffer, 1024);
if (size <= 0) {
break;
}
[data appendBytes:buffer length:size];
if (size < 1024) {
break;
}
}
NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//printf("aString = %s",[aString UTF8String]);
//NSLog(@"aString = %@",aString);
// Do something
}
});
dispatch_resume(source);
return source;
}
日志同步/上傳
重定向或者存儲(chǔ)的數(shù)據(jù)可以傳到服務(wù)端或者通過(guò)server同步到網(wǎng)頁(yè)上,就可以更方便的看到這些數(shù)據(jù)了。
如果想再網(wǎng)頁(yè)端實(shí)時(shí)查看日志,可以在App內(nèi)置一個(gè)小型http web服務(wù)器。GitHub上開(kāi)源的項(xiàng)目有GCDWebServer,可以使用該工具,在APP開(kāi)啟webserver服務(wù),并在同一局域網(wǎng)下,使用http://localhost:8080來(lái)請(qǐng)求最新日志了。
上傳服務(wù)端的部分很簡(jiǎn)單,實(shí)現(xiàn)簡(jiǎn)單的網(wǎng)絡(luò)請(qǐng)求就可以,這兒不做敘述。
另外在實(shí)際項(xiàng)目中,可以設(shè)置一個(gè)開(kāi)關(guān)來(lái)開(kāi)啟或關(guān)閉這個(gè)重定向,在調(diào)試測(cè)試的過(guò)程中可以打開(kāi)開(kāi)關(guān)來(lái)查看程序當(dāng)前的日志。
通過(guò)以上處理,真機(jī)測(cè)試中,日志就可以很方便的獲取和查看了,這樣能節(jié)省不少人力和時(shí)間成本。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
參考文檔
相關(guān)文章
iOS 條碼及二維碼掃描(從相冊(cè)中讀取條形碼/二維碼)及掃碼過(guò)程中遇到的坑
本文主要給大家介紹ios中從手機(jī)相冊(cè)中讀取條形碼和二維碼的問(wèn)題及解決辦法,需要的朋友參考下2017-01-01
IOS 開(kāi)發(fā)之swift中UIView的擴(kuò)展使用的實(shí)例
這篇文章主要介紹了IOS 開(kāi)發(fā)之swift中UIView的擴(kuò)展使用的實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09
實(shí)例解析設(shè)計(jì)模式中的外觀模式在iOS App開(kāi)發(fā)中的運(yùn)用
這篇文章主要介紹了設(shè)計(jì)模式中的外觀模式在iOS App開(kāi)發(fā)中的運(yùn)用,實(shí)例代碼為傳統(tǒng)的Objective-C,需要的朋友可以參考下2016-03-03
詳解iOS App開(kāi)發(fā)中UIViewController的loadView方法使用
這篇文章主要介紹了詳解iOS App開(kāi)發(fā)中UIViewController的loadView方法使用,講解了訪問(wèn)view屬性時(shí)loadView方法的調(diào)用及使用loadView時(shí)的一些注意點(diǎn),需要的朋友可以參考下2016-03-03
IOS開(kāi)發(fā)之路--C語(yǔ)言數(shù)組和字符串
數(shù)組在C語(yǔ)言中有著特殊的地位,它有很多特性,例如它的存儲(chǔ)是連續(xù)的,數(shù)組的名稱(chēng)就是數(shù)組的地址等。而在C語(yǔ)言中是沒(méi)有String類(lèi)型的,那么如果要表示一個(gè)字符串,就必須使用字符串?dāng)?shù)組2014-08-08
iOS 斷點(diǎn)上傳文件的實(shí)現(xiàn)方法
這項(xiàng)目開(kāi)發(fā)中,有時(shí)候我們需要將本地的文件上傳到服務(wù)器,簡(jiǎn)單的幾張圖片還好,但是針對(duì)iPhone里面的視頻文件進(jìn)行上傳,為了用戶(hù)體驗(yàn),我們有必要實(shí)現(xiàn)斷點(diǎn)上傳。這篇文章主要介紹了iOS 斷點(diǎn)上傳文件的實(shí)現(xiàn)方法,需要的朋友可以參考下2017-12-12
詳解iOS開(kāi)發(fā)中UIPickerView控件的使用方法
這篇文章主要介紹了詳解iOS開(kāi)發(fā)中UIPickerView控件的使用方法,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-11-11
iOS左右滑動(dòng)標(biāo)簽頁(yè)導(dǎo)航的設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了iOS左右滑動(dòng)標(biāo)簽頁(yè)導(dǎo)航的設(shè)計(jì)思路,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06
ios利用正則表達(dá)式判斷手機(jī)號(hào)碼格式是否正確的實(shí)例

