web实时输出iOS设备log

2017-10-21  本文已影响0人  chensifang

web实时打印iOS设备log

项目进程中, 测试人员或者开发工程师在测试机没有连接Xcode的调试状况下如果出了问题需要debug, 需要插上线连接Xcode重新run, 查看相应的log, 耗时且问题不一定能稳定复现, 现在介绍一种在网页上能实时查看iOS设备log的方法

1. 截取logString

通常工程都会自定义log, 能够取到log的具体String.
这里创建一个Log类实现:


#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
#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底层会将log写入系统文件, 影响效率.
截取到logStr以后将其添加到单例的logs数组中, 供后面用.

2. 创建一个Socket保证手机和网页能实时通信

这里我使用一个三方框架: GCDWebServer
集成方式:
在Podfile中添加pod 'GCDWebServer', 执行$pod install命令

3. 将logString通过'GCDWebServer'输出到网页

创建一个PKHttpServerLogger

#import <Foundation/Foundation.h>
@interface PKHttpServerLogger : NSObject

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

@end

一个单例方法, 开启服务和结束服务方法.

#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>无数据</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];
这句代码开启服务, 走8079端口, 注意: 这个端口可以自定义, 如果失败, 多是端口占用, 只需要再换一个就可以

下面的代码多是一些网页和H5的内容, 定义web输出的格式, iOS工程师可以直接copy使用.

这段代码会轮询[Log shared].logs中的log信息, 一旦输出完毕会立即清空, 保证既不重复也不丢失.

4. 调用开启方法

在控制器Viewdidload方法中开启服务并开启定时器输出:

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

5. 网页操作

打开浏览器, 输入ip地址, 带上上面的端口号即可.

服务启动后允许一个网络权限即可, 如下图:

图片.png

浏览器效果: 每隔2s输出一次(实时)

上一篇 下一篇

猜你喜欢

热点阅读