web實(shí)時(shí)輸出iOS設(shè)備log

web實(shí)時(shí)打印iOS設(shè)備log

項(xiàng)目進(jìn)程中, 測(cè)試人員或者開(kāi)發(fā)工程師在測(cè)試機(jī)沒(méi)有連接X(jué)code的調(diào)試狀況下如果出了問(wèn)題需要debug, 需要插上線連接X(jué)code重新run, 查看相應(yīng)的log, 耗時(shí)且問(wèn)題不一定能穩(wěn)定復(fù)現(xiàn), 現(xiàn)在介紹一種在網(wǎng)頁(yè)上能實(shí)時(shí)查看iOS設(shè)備log的方法

1. 截取logString

通常工程都會(huì)自定義log, 能夠取到log的具體String.
這里創(chuàng)建一個(gè)Log類(lèi)實(shí)現(xiàn):

  • log.h代碼如下:

#import <Foundation/Foundation.h>

#if DEBUG
#define PKLog(frmt,...) [Log logWithLine:__LINE__ method:NSStringFromSelector(_cmd) class:self.class time:[NSDate date] format:[NSString stringWithFormat:frmt, ## __VA_ARGS__]]
#else
#define PKLog(frmt,...)
#endif


@interface Log : NSObject
@property (nonatomic, strong) NSMutableArray *logs;
+ (instancetype)shared;

+ (void)logWithLine:(NSUInteger)line
             method:(NSString *)methodName
              class:(Class)className
               time:(NSDate *)timeStr
             format:(NSString *)format;

@end
  • Log.m代碼如下:
#import "Log.h"
#import "PKHttpServerLogger.h"

@implementation Log
+ (instancetype)shared {
    static dispatch_once_t onceToken;
    static Log *shared;
    dispatch_once(&onceToken, ^{
        shared = [[Log alloc] init];
    });
    return shared;
}

- (NSMutableArray *)logs {
    if (!_logs) {
        _logs = [NSMutableArray array];
    }
    return _logs;
}

+ (void)logWithLine:(NSUInteger)line
             method:(NSString *)methodName
              class:(Class)className
               time:(NSDate *)timeStr
             format:(NSString *)format {
    
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday |
    NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitNanosecond;
    NSDateComponents *comps  = [calendar components:unitFlags fromDate:[NSDate date]];
    NSString *time = [NSString stringWithFormat:@"%ld/%ld,%ld:%ld:%ld:%@", (long)comps.month, (long)comps.day, (long)comps.hour, (long)comps.minute, (long)comps.second, [[NSString stringWithFormat:@"%ld", (long)comps.nanosecond] substringToIndex:2]];
    
    NSString *logStr = [NSString stringWithFormat:@"[%@][%@ %@] %tu行: ● %@.\n", time, className,methodName,line,format];
    [[Log shared].logs addObject:logStr];
    fprintf(stderr,"[%s][%s %s] %tu行: ● %s.\n", [time UTF8String], [NSStringFromClass(className) UTF8String],[methodName UTF8String],line,[format UTF8String]);
}

定義PKLog宏截取logStr, 可以自定義log格式, 且最終用fprintf輸出比NSLog效率高, NSLog底層會(huì)將log寫(xiě)入系統(tǒng)文件, 影響效率.
截取到logStr以后將其添加到單例的logs數(shù)組中, 供后面用.

2. 創(chuàng)建一個(gè)Socket保證手機(jī)和網(wǎng)頁(yè)能實(shí)時(shí)通信

這里我使用一個(gè)三方框架: GCDWebServer
集成方式:
在Podfile中添加pod 'GCDWebServer', 執(zhí)行$pod install命令

3. 將logString通過(guò)'GCDWebServer'輸出到網(wǎng)頁(yè)

創(chuàng)建一個(gè)PKHttpServerLogger類(lèi)

  • PKHttpServerLogger.h代碼如下
#import <Foundation/Foundation.h>
@interface PKHttpServerLogger : NSObject

+ (instancetype)shared;
- (void)startServer;
- (void)stopServer;

@end

一個(gè)單例方法, 開(kāi)啟服務(wù)和結(jié)束服務(wù)方法.

  • PKHttpServerLogger.m代碼如下
#import "PKHttpServerLogger.h"
#import "GCDWebServer.h"
#import "GCDWebServerDataResponse.h"
#import "Log.h"
#define kMinRefreshDelay 500  // In milliseconds

@interface PKHttpServerLogger ()
@property (nonatomic,strong) GCDWebServer* webServer;
@end
@implementation PKHttpServerLogger

+ (instancetype)shared {
    static dispatch_once_t onceToken;
    static PKHttpServerLogger *shared;
    dispatch_once(&onceToken, ^{
        shared = [PKHttpServerLogger 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 8079
    // until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
    [self.webServer startWithPort:8079 bonjourName:nil];
    
}

- (void)stopServer {
    [_webServer stop];
    _webServer = nil;
}


- (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>無(wú)數(shù)據(jù)</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;
    [[Log shared].logs enumerateObjectsUsingBlock:^(NSString * _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];
        [[Log shared].logs removeObject:obj];
    }];
    
    [string appendFormat:@"<tr id=\"maxTime\" data-value=\"%f\"></tr>", maxTime];
    
}


- (NSString *)displayedTextForLogMessage:(NSString *)msg{
    NSMutableString *string = [[NSMutableString alloc] init];
    [string appendFormat:@"%@",msg];
    return string;
}

[self.webServer startWithPort:8079 bonjourName:nil];
這句代碼開(kāi)啟服務(wù), 走8079端口, 注意: 這個(gè)端口可以自定義, 如果失敗, 多是端口占用, 只需要再換一個(gè)就可以

下面的代碼多是一些網(wǎng)頁(yè)和H5的內(nèi)容, 定義web輸出的格式, iOS工程師可以直接copy使用.

這段代碼會(huì)輪詢(xún)[Log shared].logs中的log信息, 一旦輸出完畢會(huì)立即清空, 保證既不重復(fù)也不丟失.

4. 調(diào)用開(kāi)啟方法

在控制器Viewdidload方法中開(kāi)啟服務(wù)并開(kāi)啟定時(shí)器輸出:

[[PKHttpServerLogger shared] startServer];
    __block int num = 0;
    [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        PKLog(@"log %d", num++);
    }];

5. 網(wǎng)頁(yè)操作

打開(kāi)瀏覽器, 輸入ip地址, 帶上上面的端口號(hào)即可.

服務(wù)啟動(dòng)后允許一個(gè)網(wǎng)絡(luò)權(quán)限即可, 如下圖:

圖片.png

瀏覽器效果: 每隔2s輸出一次(實(shí)時(shí))

  • 圖片.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容