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

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


日志收集逻辑

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

1. 自定义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 最后就是将上传服务器的内容下载下来就可以进行解析了.

2. freopen

  • 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.1 同样针对某一用户是否开启日志收集(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.2 同样触发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. 异同点分析

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

四. 结语

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

作者简书

作者掘金

作者GitHub

.End

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容