web实时输出iOS设备log

web实时打印iOS设备log

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

1. 截取logString

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

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

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

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

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

创建一个PKHttpServerLogger

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

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

@end

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

  • 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>无数据</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输出一次(实时)

  • 图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,653评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,321评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,833评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,472评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,306评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,274评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,658评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,335评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,638评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,697评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,454评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,311评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,699评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,986评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,254评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,647评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,847评论 2 335

推荐阅读更多精彩内容