針對線上問題或者用戶使用流程的追蹤, 自定義日志是很不錯(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)兮莱睁,吾將上下而求索~
.End