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)限即可, 如下圖:

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