iOS APP日志寫入文件 & 上傳服務器

針對線上問題或者用戶使用流程的追蹤, 自定義日志是很不錯的解決問題的方案,主要思路就是:


日志收集邏輯

本文主要介紹兩個方案, 第一種方案是自定義Log文件,來替換NSLog來使用; 第二種是通過freopen函數(shù)將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 {
    // 名稱自定義,上傳服務器的時候記得關聯(lián)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 - 初始化異常捕獲系統(tǒng)
+ (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);
    //異常時調用的函數(shù)
    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文件在內部就區(qū)分了是否為DEBUG環(huán)境, 所以在使用上直接請求對應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 觸發(fā)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];
}

只要是打印了, 都會默認寫入到文件中, 比如沒有實現(xiàn)按鈕的點擊事件, 崩潰信息也會進行記錄:

[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()函數(shù)用于文件流的的重定向,一般是將 stdin、stdout 和 stderr 重定向到文件.

  • 所謂重定向,就是改變文件流的源頭或目的地。stdout(標準輸出流)的目的地是顯示器,printf()是將流中的內容輸出到顯示器;可以通過freopen()將stdout 的目的地改為一個文件(如output.txt),再調用 printf(),就會將內容輸出到這個文件里面,而不是顯示器.

  • freopen()函數(shù)的原型為:

FILE    *freopen(const char * __restrict, const char * __restrict,
                 FILE * __restrict) __DARWIN_ALIAS(freopen);
使用方法:
FILE *fp = freopen(“xx.txt”,“r”,stdin);//將標準輸入流重定向到xx.txt。即從xx.txt中獲取讀入。
第二個參數(shù)(模式):
“r” 打開一個用于讀取的文件。該文件必須存在。
“w” 創(chuàng)建一個用于寫入的空文件。如果文件名稱與已存在的文件相同,則會刪除已有文件的內容,文件被視為一個新的空文件。
“a” 追加到一個文件。寫操作向文件末尾追加數(shù)據。如果文件不存在,則創(chuàng)建文件。
“r+” 打開一個用于更新的文件,可讀取也可寫入。該文件必須存在。
“w+” 創(chuàng)建一個用于讀寫的空文件。
“a+” 打開一個用于讀取和追加的文件。
  • 【參數(shù)】

@return 返回值為一個指向FILE類型的指針
@param 參數(shù)分別為重定向時的文件路徑、文件訪問模式以及被重定向的流

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 同樣觸發(fā)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文件可以自定義打印格式和很多其他的拓展功能, 但是本身是基于宏定義來實現(xiàn)的, 所以對于組件化的工程不是很友好, 入侵性較大.
3.2 freopen()函數(shù)寫入呢, 原汁原味, 無依賴, 只需要判斷好觸發(fā)條件即可; 還有一個坑點就是磁盤內存的判斷, 比較惡心, 使用時注意下即可.

四. 結語

路漫漫其修遠兮,吾將上下而求索~

作者簡書

作者掘金

作者GitHub

.End

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容