iOS APP日志寫入文件 & 上傳服務(wù)器

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


日志收集邏輯

本文主要介紹兩個(gè)方案, 第一種方案是自定義Log文件,來替換NSLog來使用; 第二種是通過freopen函數(shù)將NSLog的輸出日志,重定向保存.

1. 自定義Log文件

下面是Log類, 一個(gè)開關(guān)屬性, 一個(gè)自定義Log格式輸出方法, 一個(gè)Log寫入文件方法. DEBUG的時(shí)候直接輸出到控制臺, release且Log開關(guān)開的時(shí)候?qū)懭胛募?/p>

  • 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 {
    
    // 日志時(shí)間格式化
    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 {
    // 名稱自定義,上傳服務(wù)器的時(shí)候記得關(guān)聯(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í)調(diào)用的函數(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文件在內(nèi)部就區(qū)分了是否為DEBUG環(huán)境, 所以在使用上直接請求對應(yīng)id的接口, 根據(jù)后臺設(shè)置的結(jié)果開啟本地寫入功能即可.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 可以多處放這一段代碼, 放到此處是因?yàn)檫M(jìn)后臺的操作較少, 不會在請求期間漏掉log信息
    [self requestFileLogOnOrOffWithUseId:12345 complete:^(BOOL offOrOn) {
        // 既是DEBUG用, 此處也可做成同步, 使用dispatch_semaphore CGD信號量即可, 一般不需要這么極端.
        [Log setFileLogOnOrOff:offOrOn];
    }];
    return YES;
}
1.2 觸發(fā)Log的寫入

在所有你想寫入記錄的位置, 引入頭文件, 進(jìn)行正常的NSLog打印即可, 例如:

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

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

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

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

[2022-2-21 14:28:59:32]-[ViewController viewWillAppear:] 22行: 調(diào)用了-[ViewController viewWillAppear:]方法.
[2022-2-21 14:29:4:15]-[ViewController viewDidAppear:] 27行: 調(diào)用了-[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 最后就是將上傳服務(wù)器的內(nèi)容下載下來就可以進(jìn)行解析了.

2. freopen

  • freopen()函數(shù)用于文件流的的重定向尼荆,一般是將 stdin左腔、stdout 和 stderr 重定向到文件.

  • 所謂重定向,就是改變文件流的源頭或目的地捅儒。stdout(標(biāo)準(zhǔn)輸出流)的目的地是顯示器液样,printf()是將流中的內(nèi)容輸出到顯示器;可以通過freopen()將stdout 的目的地改為一個(gè)文件(如output.txt)巧还,再調(diào)用 printf()鞭莽,就會將內(nèi)容輸出到這個(gè)文件里面,而不是顯示器.

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

FILE    *freopen(const char * __restrict, const char * __restrict,
                 FILE * __restrict) __DARWIN_ALIAS(freopen);
使用方法:
FILE *fp = freopen(“xx.txt”,“r”,stdin);//將標(biāo)準(zhǔn)輸入流重定向到xx.txt麸祷。即從xx.txt中獲取讀入澎怒。
第二個(gè)參數(shù)(模式):
“r” 打開一個(gè)用于讀取的文件。該文件必須存在摇锋。
“w” 創(chuàng)建一個(gè)用于寫入的空文件丹拯。如果文件名稱與已存在的文件相同,則會刪除已有文件的內(nèi)容荸恕,文件被視為一個(gè)新的空文件乖酬。
“a” 追加到一個(gè)文件。寫操作向文件末尾追加數(shù)據(jù)融求。如果文件不存在咬像,則創(chuàng)建文件。
“r+” 打開一個(gè)用于更新的文件生宛,可讀取也可寫入县昂。該文件必須存在。
“w+” 創(chuàng)建一個(gè)用于讀寫的空文件陷舅。
“a+” 打開一個(gè)用于讀取和追加的文件倒彰。
  • 【參數(shù)】

@return 返回值為一個(gè)指向FILE類型的指針
@param 參數(shù)分別為重定向時(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"];
    // 啟動時(shí)間為文件名稱, 可自定義
    NSString *fileName = [NSString stringWithFormat:@"LOG-%@.txt",[dateformat stringFromDate:[NSDate date]]];
    NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:fileName];
    // 先刪除已經(jīng)存在的文件
    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寫入, 日志內(nèi)容為:

2022-02-21 15:03:42.446299+0800 LogDemo[3275:2248894] 調(diào)用了-[ViewController viewWillAppear:]方法

2022-02-21 15:03:42.560687+0800 LogDemo[3275:2248894] 調(diào)用了-[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 是否上傳和下載就依據(jù)個(gè)人需求而定了.

3. 異同點(diǎn)分析

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

四. 結(jié)語

路漫漫其修遠(yuǎn)兮莱睁,吾將上下而求索~

作者簡書

作者掘金

作者GitHub

.End

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末待讳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仰剿,更是在濱河造成了極大的恐慌创淡,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件南吮,死亡現(xiàn)場離奇詭異琳彩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門露乏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碧浊,“玉大人,你說我怎么就攤上這事瘟仿』源剩” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵猾骡,是天一觀的道長。 經(jīng)常有香客問我敷搪,道長兴想,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任赡勘,我火速辦了婚禮嫂便,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闸与。我一直安慰自己毙替,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布践樱。 她就那樣靜靜地躺著厂画,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拷邢。 梳的紋絲不亂的頭發(fā)上袱院,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音瞭稼,去河邊找鬼忽洛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛环肘,可吹牛的內(nèi)容都是我干的欲虚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼悔雹,長吁一口氣:“原來是場噩夢啊……” “哼复哆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荠商,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤寂恬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后莱没,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體初肉,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年饰躲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牙咏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臼隔。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖妄壶,靈堂內(nèi)的尸體忽然破棺而出摔握,到底是詐尸還是另有隱情,我是刑警寧澤丁寄,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布氨淌,位于F島的核電站,受9級特大地震影響伊磺,放射性物質(zhì)發(fā)生泄漏盛正。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一屑埋、第九天 我趴在偏房一處隱蔽的房頂上張望豪筝。 院中可真熱鬧,春花似錦摘能、人聲如沸续崖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽严望。三九已至,卻和暖如春逻恐,著一層夾襖步出監(jiān)牢的瞬間著蟹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工梢莽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留萧豆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓昏名,卻偏偏與公主長得像涮雷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子轻局,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容