产品上线之后,总会有各种各样的环境导致会出现一些无法复现的问题,对于开发人员来说,这个时候的日志就是一个很重要的找错手段,如果能像Xcode调试的时候一样获取到所有的日志,那么至少会对我们排查错误会有一些帮助。
找到大概三种方式来记录日志:
- Apple System Log(ASL)
- 日志重定向
- 第三方框架(比较出名的 CocoaLumberjack )
Apple System Log
那么如何来记录日志呢,其实我们的日志主要是通过NSLog来输出到控制台的,所以从这里作为切入点找到记录日志的方法。
NSLog - Foundation | Apple Developer Documentation官方文档对NSLog的说明是
Logs an error message to the Apple System Log facility.
也就是说他会被作为error信息输出到Apple System Log中去了。
Apple System Log,以下简称ASL,是苹果自己实现的用于输出日志到系统日志库的一套API接口,在iOS真机设备上,ASL记录的log被缓存在本地沙盒中。
但是ASL在iOS 10开始,被Apple限制,废弃了ASLlink,所以在iOS 10之后的版本中,ASL变得不是那么方便。
日志重定向
不过通过官方文档中可以看到其实NSLog是将内容输出到文件中,然后打印出来,那么IO流的东西就会有文件的句柄了。
可以看到,在C语言中的三个默认句柄:
#define stdin __stdinp
#define stdout __stdoutp
#define stderr __stderrp
在OC当中也有对应的三个:
#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */
接下来,除了NSLog,还有c的printf输出。
printf会向标准输出stdout
打印;
NSLog会向标准错误stderr
打印;
既然是IO流的操作,我们就可以通过重定向将输出到控制台的内容保存到我们想要保存的路径中去。
freopen()
通过freopen()函数,我们可以将控制台打印的内容重定向到我们指定的文件中,但是重定向的一个问题就是会导致控制台中无法打印出任何内容,因为所有的输出都输出到了我们制定的内容中去了。
+ (void)redirectNSLogToDocumentFile
{
if([RedirectLogManager isXcodeDebugging] ||
[RedirectLogManager isSimulator])
{
return;
}
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *logPath = [[paths firstObject] stringByAppendingString:[NSString stringWithFormat:@“/%@“,FILE_NAME]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:logPath])
{
[fileManager removeItemAtPath:logPath error:nil];
}
freopen([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
freopen([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
}
+ (BOOL)isXcodeDebugging
{
//判断是否是Xcode调试状态
return isatty(STDOUT_FILENO);
}
+ (BOOL)isSimulator
{
//判断是否是Simulator
UIDevice *device = [UIDevice currentDevice];
return [[device model] hasSuffix:@“Simlator”];
}
这里我们对stdout和stderr都进行了重定向,将他们输出到我们指定的Document目录下的xxx.log文件中(xxx是你自己定义的名字)。另外为了防止在调试过程中和在模拟器中看不到Xcode中的调试内容,在这里做了判断,在Xcode中和模拟器环境中不进行重定向。
同样的,取出这部分内容也很容易,直接通过文件系统就可以取出来了。
+ (NSString *)getNSLogDocumentFileString
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *logPath = [[paths firstObject] stringByAppendingString:[NSString stringWithFormat:@“/%@“,FILE_NAME]];
NSData *fileData = [NSData dataWithContentsOfFile:logPath];
NSString *fileString = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
return fileString;
}
接下来只需要在AppDelegate中当应用启动时调用就可以了。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
…
[RedirectLogManager redirectNSLogToDocumentFile];
…
}
dup2
(转摘自iOS 日志获取和实时浏览器显示日志 - iOS - 掘金)
- (void)redirectSTD:(int )fd{
NSPipe * pipe = [NSPipe pipe] ;
NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;
int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
dup2(pipeFileHandle, fd) ;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(redirectNotificationHandle:)
name:NSFileHandleReadCompletionNotification
object:pipeReadHandle] ;
[pipeReadHandle readInBackgroundAndNotify];
}
- (void)redirectNotificationHandle:(NSNotification *)nf{
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
//这里可以做我们需要的操作,例如将nslog显示到一个textview中,或者是存放到另一个文件中等等
//self.logTextView.text = [NSString stringWithFormat:@"%@\n%@",self.logTextView.text, str];
NSRange range;
//range.location = [self.logTextView.text length] - 1;
range.length = 0;
//[self.logTextView scrollRangeToVisible:range];
[[nf object] readInBackgroundAndNotify];
}
使用:
[self redirectSTD:STDERR_FILENO];
dispatch_source
(转摘自iOS 日志获取和实时浏览器显示日志 - iOS - 掘金)
- (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
char* buffer = malloc(1024);
NSMutableData* data = [[NSMutableData alloc] init];
fcntl(fd, F_SETFL, O_NONBLOCK);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_cancel_handler(source, ^{
free(buffer);
});
dispatch_source_set_event_handler(source, ^{
@autoreleasepool {
while (1) {
ssize_t size = read(fd, buffer, 1024);
if (size <= 0)=“” {=“” break;=“” }=“” [data=“” appendbytes:buffer=“” length:size];=“” if=“” (size=“” <=“” 1024)=“” nsstring=“” *astring=“[[NSString” alloc]=“” initwithdata:data=“” encoding:nsutf8stringencoding];=“” printf(“astring=“%s”,[aString” utf8string]);=“” nslog(@“astring=“%@“,aString);” 读到了日志,可以进行我们需要的各种操作了=“” });=“” dispatch_resume(source);=“” return=“” source;=“” code=“”>
使用时:
_sourt_t = [self _startCapturingWritingToFD:STDERR_FILENO];
第三方库
这里主要以GitHub - CocoaLumberjack/CocoaLumberjack: A fast & simple, yet powerful & flexible logging framework for Mac and iOS为代表的一些第三方库,具体的就不多介绍了。需要的可以直接去GitHub找找。
最后
以上就是对iOS中日志记录的一些简单的方法,主要是以freopen方法重定向为主,其他的主要是根据其他大佬的文章看了看。
本文章仅限个人学习使用,如果有什么问题还请大佬多多指教。
参考文档
Logging | Apple Developer Documentation
Activity Tracing | Apple Developer Documentation
1.日志监控 - 小专栏
iOS 日志保存 - 掘金
iOS 日志获取和实时浏览器显示日志 - iOS - 掘金
Apple系统日志捕获方案 - 开不了口的猫的技术博客
iOS – NSLog、UncaughtException日志保存到文件 - 程序园
iOS - How can I capture iPhone console logs programmatically? - Stack Overflow
iOS - Read logs using the new swift os_log api - Stack Overflow