iOS日誌獲取和實時瀏覽器顯示日誌
平時我們寫程式碼的時候,為了除錯方便,總是會在程式碼中寫入很多的NSLog(也可能是其它的日誌框架等,例如大名鼎鼎的CocoaLumberjack),但是我們對於NSLog到底瞭解多少?NSLog的資訊為什麼Xcode能夠獲取的到?我們能自己寫個程式獲取所有的NSlog麼?NSLog寫入的資訊到底在哪裡?
NSLog輸出到哪?
我們都知道,NSLog是一個C函式,它的函式宣告是
void NSLog(NSString *format, ...)
系統對其說明是:Logs an error message to the Apple System Log facility.
這裡提到的ASL,都是放在ash.h這個標頭檔案中,這套api可以獲取指定的日誌資料.具體可以參考ASL參考
從上面可以直到,NSLog預設被系統輸出到了一個檔案中,這個檔案是哪個呢?NSLog預設的輸出到了系統的 /var/log/syslog這個檔案中,當然了,如果你的機器沒有越獄,你是檢視不了這個檔案的.我手機是越獄的,於是乎驗證了下,使用iTools等工具將真機的/var/log/syslog檔案匯出,下面就是這個檔案的部分內容的擷取
從中,我們可以看到,所有的APP的NSLog全部都是寫到這個檔案中的!!!
標準的err控制檯
我們現在瞭解到了NSLog就是輸出到檔案syslog中,既然要往檔案中寫,那麼肯定就有檔案的控制代碼了,這個檔案的控制代碼是多少呢?
在C語言中,我們有三個預設的控制代碼
#define stdin __stdinp
#define stdout __stdoutp
#define stderr __stderrp
其對應的iOS系統層面的上述三個控制代碼其實也就是下面的三個
#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");
在Xcode的控制檯可以看到輸出
2016-06-15 12:57:17.286 TestNSlog[68073:1441419] ViewController viewDidLoad
ViewController viewDidLoad222
由於fprintf並不會像NSLog那樣,在內部呼叫ASL介面,所以只是單純的輸出資訊,並沒有新增日期,程序名,程序id等,也不會自動換行.
NSLog的重定向
既然NSLog是寫到STDERR_FILENO中去的,那麼根據Unix的知識,我們可以重定向這個檔案,讓NSLog直接寫到檔案中去
//to log to document directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/mylog.log"];
//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
利用c語言的freopen函式,進行重定向,將寫往stderr的內容重定向到我們制定的檔案中去,一旦執行了上述程式碼,那麼在這個之後的NSLog將不會在控制檯顯示了,會直接輸出在檔案mylog.log中!
在模擬器中,我們可以使用終端的tail命令(tail -f mylog.log)對這個檔案進行實時檢視,就如同我們在xcode的輸出視窗中看到的那樣,你還可以結合grep命令進行實時過濾檢視,非常方便在大量的日誌資訊中迅速定位到我們要的日誌資訊
在真機中,這種重定向有什麼用處呢? 由於重定向到的檔案是我們沙盒中的檔案,那麼就可以在我們的程式中寫一段程式碼將這個檔案傳送給我們,遠端的使用者app出了問題,把日誌傳送給我們,我們就可以根據日誌資訊,找尋可能的問題所在!
也可以開啟app的資料夾itunse共享
配置共享資料夾:
在應用程式的Info.plist檔案中新增UIFileSharingEnabled鍵,並將鍵值設定為YES。將您希望共享的檔案放在應用程式的Documents目錄。一旦裝置插入到使用者計算機,iTunes 9.1就會在選中裝置的Apps標籤中顯示一個File Sharing區域。此後,使用者就可以向該目錄新增檔案或者將檔案移動到桌面計算機中
就是說,一旦裝置連線上電腦,可以通過iTune檢視指定應用程式的共享資料夾,將檔案拷貝到你的電腦上看
一般我們都會在應用中放置一個開關,開啟或者關閉Log日誌的重定向,在上面,我們使用標準C的freopen將stderr重定向到我們的檔案中了,那麼問題來了,怎麼重定向回去呢???
FILE * freopen ( const char * filename, const char * mode, FILE * stream );
要想重定向回去,那麼我們需要知道stderr原來的檔案路徑,很遺憾,這個在不同平臺中是不一樣的,在iOS平臺,由於沙盒機制,我們也並不能直接使用沙盒外的檔案
對此,freopen將無能為力,要重定向回去,只能使用Unix的方法dup和dup2!
//在ios上可用的方式,還是得藉助dup和dup2
int originH1 = dup(STDERR_FILENO);
FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);//這句話已經重定向了,現在NSLog都輸出到檔案中去了,
//……………….
//恢復原來的
dup2(originH1, STDERR_FILENO);//就可以了
其它重定向STDERR_FILENO的方式集錦
方式一 採用dup2的重定向方式
- (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] ;
//這裡可以做我們需要的操作,例如將nslog顯示到一個textview中,或者是存放到另一個檔案中等等
//self.logTextView.text = [NSString stringWithFormat:@"%@\n%@",self.logTextView.text, str];
NSRange range;
//range.location = [self.logTextView.text length] - 1;
range.length = 0;
//[self.logTextView scrollRangeToVisible:range];
[[nf object] readInBackgroundAndNotify];
}
使用的時候
[self redirectSTD:STDERR_FILENO];
就可以將NSLOg的輸出重定向到我們的通知中去!!!
方式二 使用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);
//讀到了日誌,可以進行我們需要的各種操作了
}
});
dispatch_resume(source);
return source;
}
使用的時候
_sourt_t = [self _startCapturingWritingToFD:STDERR_FILENO];
記得,要自己保留返回的dispatch_source_t物件,不然其釋放了,你就獲取不到了!
ASL讀取日誌
以上的方式,都是重定向檔案,一旦重定向後,那麼NSLog就不會再寫到系統的syslog中去了,也就意味著不能使用ASL介面獲取到重定向後的資料了.
不重定向NSLog,怎麼讀取所有的log呢?
ASL讀取log的核心程式碼
+ (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;
}
//這個是怎麼從日誌的物件aslmsg中獲取我們需要的資料
+(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寫入的文字內容
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
logMessage.messageID = [@(messageID) longLongValue];
}
return logMessage;
}
ASL的好處是沒有重定向檔案,所以不會影響Xcode等控制檯的輸出,它是一種非侵入式的讀取的方式,類似於我們讀取資料庫的檔案,我們只是讀取資料,並沒有將原來的資料庫檔案刪除.
在app中內建一個小型的http web伺服器
上面的方式,當測試,或者平時我們沒有連線XCode時,想檢視日誌資訊,還是不太方便,試想,如果我們在需要的時候,可以直接用瀏覽器檢視輸出的log資訊那該多好?
結合上面的ASL和一個小型的web伺服器,我們就可以實現了,
對於httpserver
github上比較知名的有
CocoaHTTPServer,這個已經三年沒更新了,不推薦使用
GCDWebServer 作者一直在維護,據說效能也不錯,推薦使用這個,下面的demo也使用的這個
摘錄其中的部分程式碼如下:
#define kMinRefreshDelay 500 // In milliseconds
@interface HttpServerLogger ()
@property (nonatomic,strong) GCDWebServer* webServer;
@end
@implementation HttpServerLogger
+ (instancetype)shared {
static dispatch_once_t onceToken;
static HttpServerLogger *shared;
dispatch_once(&onceToken, ^{
shared = [HttpServerLogger new];
});
return shared;
}
- (GCDWebServer *)webServer {
if (!_webServer) {
_webServer = [[GCDWebServer alloc] init];
__weak __typeof__(self) weakSelf = self;
// Add a handler to respond to GET requests on any URL
[_webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [weakSelf createResponseBody:request];
}];
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
}
return _webServer;
}
- (void)startServer{
// Use convenience method that runs server on port 8080
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
[self.webServer startWithPort:8080 bonjourName:nil];
}
- (void)stopServer {
[_webServer stop];
_webServer = nil;
}
//當瀏覽器請求的時候,返回一個由日誌資訊組裝成的html返回給瀏覽器
- (GCDWebServerDataResponse *)createResponseBody :(GCDWebServerRequest* )request{
GCDWebServerDataResponse *response = nil;
NSString* path = request.path;
NSDictionary* query = request.query;
//NSLog(@"path = %@,query = %@",path,query);
NSMutableString* string;
if ([path isEqualToString:@"/"]) {
string = [[NSMutableString alloc] init];
[string appendString:@"<!DOCTYPE html><html lang=\"en\">"];
[string appendString:@"<head><meta charset=\"utf-8\"></head>"];
[string appendFormat:@"<title>%s[%i]</title>", getprogname(), getpid()];
[string appendString:@"<style>\
body {\n\
margin: 0px;\n\
font-family: Courier, monospace;\n\
font-size: 0.8em;\n\
}\n\
table {\n\
width: 100%;\n\
border-collapse: collapse;\n\
}\n\
tr {\n\
vertical-align: top;\n\
}\n\
tr:nth-child(odd) {\n\
background-color: #eeeeee;\n\
}\n\
td {\n\
padding: 2px 10px;\n\
}\n\
#footer {\n\
text-align: center;\n\
margin: 20px 0px;\n\
color: darkgray;\n\
}\n\
.error {\n\
color: red;\n\
font-weight: bold;\n\
}\n\
</style>"];
[string appendFormat:@"<script type=\"text/javascript\">\n\
var refreshDelay = %i;\n\
var footerElement = null;\n\
function updateTimestamp() {\n\
var now = new Date();\n\
footerElement.innerHTML = \"Last updated on \" + now.toLocaleDateString() + \" \" + now.toLocaleTimeString();\n\
}\n\
function refresh() {\n\
var timeElement = document.getElementById(\"maxTime\");\n\
var maxTime = timeElement.getAttribute(\"data-value\");\n\
timeElement.parentNode.removeChild(timeElement);\n\
\n\
var xmlhttp = new XMLHttpRequest();\n\
xmlhttp.onreadystatechange = function() {\n\
if (xmlhttp.readyState == 4) {\n\
if (xmlhttp.status == 200) {\n\
var contentElement = document.getElementById(\"content\");\n\
contentElement.innerHTML = contentElement.innerHTML + xmlhttp.responseText;\n\
updateTimestamp();\n\
setTimeout(refresh, refreshDelay);\n\
} else {\n\
footerElement.innerHTML = \"<span class=\\\"error\\\">Connection failed! Reload page to try again.</span>\";\n\
}\n\
}\n\
}\n\
xmlhttp.open(\"GET\", \"/log?after=\" + maxTime, true);\n\
xmlhttp.send();\n\
}\n\
window.onload = function() {\n\
footerElement = document.getElementById(\"footer\");\n\
updateTimestamp();\n\
setTimeout(refresh, refreshDelay);\n\
}\n\
</script>", kMinRefreshDelay];
[string appendString:@"</head>"];
[string appendString:@"<body>"];
[string appendString:@"<table><tbody id=\"content\">"];
[self _appendLogRecordsToString:string afterAbsoluteTime:0.0];
[string appendString:@"</tbody></table>"];
[string appendString:@"<div id=\"footer\"></div>"];
[string appendString:@"</body>"];
[string appendString:@"</html>"];
}
else if ([path isEqualToString:@"/log"] && query[@"after"]) {
string = [[NSMutableString alloc] init];
double time = [query[@"after"] doubleValue];
[self _appendLogRecordsToString:string afterAbsoluteTime:time];
}
else {
string = [@" <html><body><p>無資料</p></body></html>" mutableCopy];
}
if (string == nil) {
string = [@"" mutableCopy];
}
response = [GCDWebServerDataResponse responseWithHTML:string];
return response;
}
- (void)_appendLogRecordsToString:(NSMutableString*)string afterAbsoluteTime:(double)time {
__block double maxTime = time;
NSArray<SystemLogMessage *> *allMsg = [SystemLogManager allLogAfterTime:time];
[allMsg enumerateObjectsUsingBlock:^(SystemLogMessage * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
const char* style = "color: dimgray;";
NSString* formattedMessage = [self displayedTextForLogMessage:obj];
[string appendFormat:@"<tr style=\"%s\">%@</tr>", style, formattedMessage];
if (obj.timeInterval > maxTime) {
maxTime = obj.timeInterval ;
}
}];
[string appendFormat:@"<tr id=\"maxTime\" data-value=\"%f\"></tr>", maxTime];
}
- (NSString *)displayedTextForLogMessage:(SystemLogMessage *)msg{
NSMutableString *string = [[NSMutableString alloc] init];
[string appendFormat:@"<td>%@</td> <td>%@</td> <td>%@</td>",[SystemLogMessage logTimeStringFromDate:msg.date ],msg.sender, msg.messageText];
return string;
}
@end
使用的時候,開啟webserver服務,在同一個區域網下, 使用 http://機子的ip:8080來請求