iOS APP日志写入文件 & 上传服务器

针对线上问题或者用户使用流程的追踪, 自定义日志是很不错的解决问题的方案,主要思路就是: 日志收集逻辑

本文主要介绍两个方案, 第一种方案是自定义Log文件,来替换NSLog来使用; 第二种是通过freopen函数将NSLog的输出日志,重定向保存.

一、自定义Log文件

下面是Log类, 一个开关属性, 一个自定义Log格式输出方法, 一个Log写入文件方法. DEBUG的时候直接输出到控制台, release且Log开关开的时候写入文件

  • Log.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

#define NSLog(frmt,...) [Log logWithLine:__LINE__ method:[NSString stringWithFormat:@"%s", __FUNCTION__] time:[NSDate date] format:[NSString stringWithFormat:frmt, ## __VA_ARGS__]]

@interface Log : NSObject

+ (void)setFileLogOnOrOff:(BOOL)on;
+ (void)logWithLine:(NSUInteger)line
             method:(NSString *)methodName
               time:(NSDate *)timeStr
             format:(NSString *)format;

@end

NS_ASSUME_NONNULL_END
复制代码
  • Log.m
#import "Log.h"

@implementation Log

static BOOL _fileLogOnOrOff;

+ (void)setFileLogOnOrOff:(BOOL)on {
    _fileLogOnOrOff = on;
    [Log initHandler];
}

+ (void)logWithLine:(NSUInteger)line
             method:(NSString *)methodName
               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:%ld:%@", (long)comps.year, (long)comps.month, (long)comps.day, (long)comps.hour, (long)comps.minute, (long)comps.second, [[NSString stringWithFormat:@"%ld", (long)comps.nanosecond] substringToIndex:2]];
#if DEBUG
    // debug 直接输出
    fprintf(stderr,"%s %s %tu行: %s.\n", [time UTF8String],[methodName UTF8String],line,[format UTF8String]);
#else
    // release && 文件Log开 写入文件
    if (_fileLogOnOrOff) {
        NSString *logStr = [NSString stringWithFormat:@"[%@]%@ %tu行: ● %@.\n", time, methodName,line,format];
        [self writeLogWithString:logStr];
    }
#endif
}

+ (void)writeLogWithString:(NSString *)content {
    // 名称自定义,上传服务器的时候记得关联userId就可以, 便于下载
    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"custom_log.text"];
    NSError *error = nil;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    // 如果不存在
    if(![fileManager fileExistsAtPath:filePath]) {
        [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
        if (error) {
            NSLog(@"文件写入失败 errorInfo: %@", error.domain);
        }
    }
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
    [fileHandle seekToEndOfFile];
    NSData* stringData  = [content dataUsingEncoding:NSUTF8StringEncoding];
    [fileHandle writeData:stringData]; // 追加
    [fileHandle synchronizeFile];
    [fileHandle closeFile];
}

#pragma mark - 初始化异常捕获系统
+ (void)initHandler {
    struct sigaction newSignalAction;
    memset(&newSignalAction, 0,sizeof(newSignalAction));
    newSignalAction.sa_handler = &signalHandler;
    sigaction(SIGABRT, &newSignalAction, NULL);
    sigaction(SIGILL, &newSignalAction, NULL);
    sigaction(SIGSEGV, &newSignalAction, NULL);
    sigaction(SIGFPE, &newSignalAction, NULL);
    sigaction(SIGBUS, &newSignalAction, NULL);
    sigaction(SIGPIPE, &newSignalAction, NULL);
    //异常时调用的函数
    NSSetUncaughtExceptionHandler(&handleExceptions);
}

void signalHandler(int sig) {
  // 打印crash信号信息
    NSLog(@"signal = %d", sig);
}

void handleExceptions(NSException *exception) {
    NSLog(@"exception = %@",exception);
    // 打印堆栈信息
    NSLog(@"callStackSymbols = %@",[exception callStackSymbols]);
}

@end
复制代码

1.1 Log文件在内部就区分了是否为DEBUG环境, 所以在使用上直接请求对应id的接口, 根据后台设置的结果开启本地写入功能即可.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 可以多处放这一段代码, 放到此处是因为进后台的操作较少, 不会在请求期间漏掉log信息
    [self requestFileLogOnOrOffWithUseId:12345 complete:^(BOOL offOrOn) {
        // 既是DEBUG用, 此处也可做成同步, 使用dispatch_semaphore CGD信号量即可, 一般不需要这么极端.
        [Log setFileLogOnOrOff:offOrOn];
    }];
    return YES;
}
复制代码

1.2 触发Log的写入

在所有你想写入记录的位置, 引入头文件, 进行正常的NSLog打印即可, 例如:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"调用了%s方法", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"调用了%s方法", __func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}
复制代码

只要是打印了, 都会默认写入到文件中, 比如没有实现按钮的点击事件, 崩溃信息也会进行记录:

[2022-2-21 14:28:59:32]-[ViewController viewWillAppear:] 22行: 调用了-[ViewController viewWillAppear:]方法.

[2022-2-21 14:29:4:15]-[ViewController viewDidAppear:] 27行: 调用了-[ViewController viewDidAppear:]方法. [2022-2-21 14:29:5:19]handleExceptions 83行: exception = -[ViewController buttonClick]: unrecognized selector sent to instance 0x7f7adda07690. [2022-2-21 14:29:5:19]handleExceptions 85行: callStackSymbols = ( 0 CoreFoundation 0x000000010f38cbb4 __exceptionPreprocess + 242 1 libobjc.A.dylib 0x000000010f240be7 objc_exception_throw + 48 2 CoreFoundation 0x000000010f39b821 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0 3 UIKitCore 0x000000011a57ff90 -[UIResponder doesNotRecognizeSelector:] + 264 4 CoreFoundation 0x000000010f3910bc forwarding + 1433 ...... ######1.4 最后就是将上传服务器的内容下载下来就可以进行解析了.

二、freopen

2.1 原理阐述:

  • freopen()函数用于文件流的的重定向,一般是将 stdin、stdout 和 stderr 重定向到文件.

  • 所谓重定向,就是改变文件流的源头或目的地。stdout(标准输出流)的目的地是显示器,printf()是将流中的内容输出到显示器;可以通过freopen()将stdout 的目的地改为一个文件(如output.txt),再调用 printf(),就会将内容输出到这个文件里面,而不是显示器.

  • freopen()函数的原型为:

FILE	*freopen(const char * __restrict, const char * __restrict,
                 FILE * __restrict) __DARWIN_ALIAS(freopen);
复制代码
使用方法:
FILE *fp = freopen(“xx.txt”,“r”,stdin);//将标准输入流重定向到xx.txt。即从xx.txt中获取读入。
第二个参数(模式):
“r” 打开一个用于读取的文件。该文件必须存在。
“w” 创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。
“a” 追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
“r+” 打开一个用于更新的文件,可读取也可写入。该文件必须存在。
“w+” 创建一个用于读写的空文件。
“a+” 打开一个用于读取和追加的文件。
复制代码
  • 【参数】

@return 返回值为一个指向FILE类型的指针

@param 参数分别为重定向时的文件路径、文件访问模式以及被重定向的流

2.2 同样针对某一用户是否开启日志收集(release&后台开启):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self requestFileLogOnOrOffWithUseId:12345 complete:^(BOOL offOrOn) {
#ifdef DEBUG
#else
        [self redirectNSlogToDocumentFolder];
#endif
    }];
    return YES;
}
复制代码
#pragma mark - 日志收集
- (void)redirectNSlogToDocumentFolder {
    NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSDateFormatter *dateformat = [[NSDateFormatter  alloc]init];
    [dateformat setDateFormat:@"yyyy-MM-dd-HH-mm-ss"];
    // 启动时间为文件名称, 可自定义
    NSString *fileName = [NSString stringWithFormat:@"LOG-%@.txt",[dateformat stringFromDate:[NSDate date]]];
    NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:fileName];
    // 先删除已经存在的文件
    NSFileManager *defaultManager = [NSFileManager defaultManager];
    [defaultManager removeItemAtPath:logFilePath error:nil];
    // 将log输入到文件
    freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
    freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
}
复制代码

2.3 同样触发1.2Log写入, 日志内容为:

2022-02-21 15:03:42.446299+0800 LogDemo[3275:2248894] 调用了-[ViewController viewWillAppear:]方法

2022-02-21 15:03:42.560687+0800 LogDemo[3275:2248894] 调用了-[ViewController viewDidAppear:]方法 2022-02-21 15:05:10.752340+0800 LogDemo[3275:2248894] -[ViewController buttonClick]: unrecognized selector sent to instance 0x7f96b4107ba0 2022-02-21 15:05:10.760077+0800 LogDemo[3275:2248894] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController buttonClick]: unrecognized selector sent to instance 0x7f96b4107ba0' *** First throw call stack: ( 0 CoreFoundation 0x0000000101d86bb4 __exceptionPreprocess + 242 1 libobjc.A.dylib 0x0000000101c3abe7 objc_exception_throw + 48 2 CoreFoundation 0x0000000101d95821 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0 3 UIKitCore 0x0000000107396f90 -[UIResponder doesNotRecognizeSelector:] + 264 4 CoreFoundation 0x0000000101d8b0bc forwarding + 1433 ...... ######2.3 是否上传和下载就依据个人需求而定了.

三、异同点分析

3.1 自定义log文件可以自定义打印格式和很多其他的拓展功能, 但是本身是基于宏定义来实现的, 所以对于组件化的工程不是很友好, 入侵性较大. 3.2 freopen()函数写入呢, 原汁原味, 无依赖, 只需要判断好触发条件即可; 还有一个坑点就是磁盘内存的判断, 比较恶心, 使用时注意下即可.

四、结语

路漫漫其修远兮,吾将上下而求索~

作者简书

作者掘金

作者GitHub

.End

Guess you like

Origin juejin.im/post/7067071579162673166