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地址, 带上上面的端口号即可.
服务启动后允许一个网络权限即可, 如下图:
浏览器效果: 每隔2s输出一次(实时)