需求:获取 app 运行时输出日志,包含接入的其他 SDK 的输出,方便开发人员在出现问题的时候,得到详细的Log信 息,快速的识别出问题的原因并修复和优化等。 一番 google 后有两种方案,一种是非侵入性的,不影响 app 连接 xcode 调试时各种信息的输出,一种是重定向,会导致输出信息在调试时看不到,各有优缺点。 一、ASL 直接获取 我们可以通过官方文档了解到,OC中最常见的NSLog操作会同时将标准的Error输出到控制台和系统日志(syslog)中(C语言的printf 系列函数并不会,swift的printf为了保证性能也只会在模拟器环境中输出)。其内部是使用Apple System Logger(简称ASL)去实现的,ASL是苹果自己实现的用于输出日志到系统日志库的一套API接口,有点类似于SQL操作。在iOS真机设备上,使用ASL记录的 log被缓存在沙盒文件中,直到设备被重启,一般会输出到系统的 /var/log/syslog 文件中。
#import <asl.h>
+(NSArray*)console
{
NSMutableArray *consoleLog = [NSMutableArray array];
aslclient client = asl_open(NULL, NULL, ASL_OPT_STDERR);
aslmsg query = asl_new(ASL_TYPE_QUERY);
asl_set_query(query, ASL_KEY_MSG, NULL, ASL_QUERY_OP_NOT_EQUAL);
aslresponse response = asl_search(client, query);
asl_free(query);
aslmsg message;
while((message = asl_next(response)) != NULL)
{
SystemLogMessage *model = [self logMessageFromASLMessage:message];
[consoleLog addObject:model];
}
if (message != NULL) {
asl_free(message);
}
asl_free(response);
asl_close(client);
return consoleLog;
}
//这个是怎么从日志的对象aslmsg中获取我们需要的数据
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
{
SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
if (timestamp) {
NSTimeInterval timeInterval = [@(timestamp) integerValue];
const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
if (nanoseconds) {
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
}
logMessage.timeInterval = timeInterval;
NSDateFormatter *format = [NSDateFormatter new];
format.dateFormat = @"YYYY-MM-dd hh:mm:ss";
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
logMessage.dateTime = [format stringFromDate:logMessage.date];
}
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
if (sender) {
logMessage.sender = @(sender);
}
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
if (messageText) {
logMessage.messageText = @(messageText);//NSLog写入的文本内容
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
logMessage.messageID = [@(messageID) longLongValue];
}
return logMessage;
}
复制代码
但是Apple从iOS 10开始,为了减弱ASL对于日志系统的侵入性,直接废弃掉了ASLlink,导致在iOS 10之后的系统版本中无法使用 ASL相关的API。因此为了能够在iOS 10之后的版本中同样获取日志数据,我们寻找一种版本兼容的解决方案。 二、重定向 1、文件重定向 利用c语言的 freopen 函数进行重定向,将写往 stderr 的内容重定向到我们制定的文件中去,一旦执行了上述代码那么在这个之后的NSLog将不会在控制台显示了,会直接输出在指定的文件中。
// 最简单的重定向
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
filePath = [filePath stringByAppendingPathComponent:[AutoTestEngine defaultEngine].uuid];
NSString *loggingPath = [filePath stringByAppendingPathComponent:@"result.log"];
//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
复制代码
2、dup2 重定向 通过 NSPipe 创建一个管道,pipe有读端和写端,然后通过 dup2 将标准输入重定向到pipe的写端。再通 过 NSFileHandle 监听pipe的读端,最后再处理读出的信息。 之后通过 printf 或者 NSLog 写数据,都会写到pipe的写端,同时pipe会将这些数据直接传送到读端,最后通过NSFileHandle的监控函数取出这些数据,这时可以随意处理,将这些数据写入指定文件。
- (void)redirectStandardOutput{
//记录标准输出及错误流原始文件描述符
outFd = dup(STDOUT_FILENO);
errFd = dup(STDERR_FILENO);
stdout->_flags = 10;
NSPipe *outPipe = [NSPipe pipe];
NSFileHandle *pipeOutHandle = [outPipe fileHandleForReading];
dup2([[outPipe fileHandleForWriting] fileDescriptor], STDOUT_FILENO);
[pipeOutHandle readInBackgroundAndNotify];
stderr->_flags = 10;
NSPipe *errPipe = [NSPipe pipe];
NSFileHandle *pipeErrHandle = [errPipe fileHandleForReading];
dup2([[errPipe fileHandleForWriting] fileDescriptor], STDERR_FILENO);
[pipeErrHandle readInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(redirectOutNotificationHandle:)
name:NSFileHandleReadCompletionNotification
object:pipeOutHandle];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(redirectErrNotificationHandle:)
name:NSFileHandleReadCompletionNotification
object:pipeErrHandle];
@autoreleasepool {
CFRunLoopRun();
}
}
-(void)recoverStandardOutput{
if (!outFd) {
return;
}
// 恢复至原来路径
dup2(outFd, STDOUT_FILENO);
dup2(errFd, STDERR_FILENO);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 重定向之后的NSLog输出
- (void)redirectOutNotificationHandle:(NSNotification *)nf
{
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// YOUR CODE HERE... 保存日志并上传或展示
if (str) {
[self writefile:str];
[[nf object] readInBackgroundAndNotify];
}
}
// 重定向之后的错误输出
- (void)redirectErrNotificationHandle:(NSNotification *)nf
{
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// YOUR CODE HERE... 保存日志并上传或展示
if (str) {
[self writefile:str];
[[nf object] readInBackgroundAndNotify];
}
}
- (void)writefile:(NSString *)string
{
NSString *filePath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
filePath = [filePath stringByAppendingPathComponent:[AutoTestEngine defaultEngine].uuid];
filePath = [filePath stringByAppendingPathComponent:@"result.log"];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
[@"" writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
[fileHandle seekToEndOfFile]; //将节点跳到文件的末尾
NSData* stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle writeData:stringData]; //追加写入数据
[fileHandle closeFile];
}
复制代码
3、dispatch_source_t 重定向
- (dispatch_source_t)startCapturingWritingToFD:(int)fd {
int fildes[2];
pipe(fildes); // [0] is read end of pipe while [1] is write end
dup2(fildes[1], fd); // Duplicate write end of pipe "onto" fd (this closes fd)
close(fildes[1]); // Close original write end of pipe
fd = fildes[0]; // We can now monitor the read end of the pipe
NSMutableData* data = [[NSMutableData alloc] init];
fcntl(fd, F_SETFL, O_NONBLOCK);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, _queue);
dispatch_source_set_cancel_handler(source, ^{
});
dispatch_source_set_event_handler(source, ^{
@autoreleasepool {
size_t estimatedSize = dispatch_source_get_data(source) + 1;
char *buffer = (char *)malloc(estimatedSize);
if (buffer) {
ssize_t actual = read(fd, buffer, (estimatedSize));
// do something with buffer
[data appendBytes:buffer length:actual];
NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"aString = %@",aString);
[self writefile:aString];
free(buffer);
[data resetBytesInRange:NSMakeRange(0, data.length)];
[data setLength:0];
//读取完毕后
// dispatch_source_cancel(source);
}
}
});
dispatch_resume(source);
return source;
}
复制代码
在短视频应用中,上面3种重定向方式均有问题,可能是短视频app的特殊性,方案1中在app中不生效,方案2在和短视频运行中会导致app卡住,方案3获取到的信息不正确且会卡住,生成文件很大。
方案1不生效无法下手,方案2,3尝试解决,查看文档,发现方案2的通知回调必须在消息循环激活中才生效,因此,创建线程,开启消息循环,将监听,读写操作均放在该线程中,解决了同时运行卡住问题,暂时采取方案2来获取app 运行时的输出日志。