利用NSStream、NSOutputStream源葫、NSInputStream寫文件诗越,一句話實現(xiàn)實時動態(tài)打日志,UIDocumentInteractionController 分享日志文件息堂。
一句話實現(xiàn)實時動態(tài)打日志
一嚷狞、在開發(fā)中我們會遇到各種各樣的問題,各式的坑輪番轟炸储矩。個人認(rèn)為基本上大部分是數(shù)據(jù)處理和數(shù)據(jù)對接造成的比較多感耙,也比較難找,如果是聯(lián)機(jī)調(diào)試持隧,那么還好找一點,但是如果是打包給測試同事逃片,或者是線上的奔潰屡拨,那么跟蹤數(shù)據(jù)就變得不那么好找了。
要是有數(shù)據(jù)日志記錄該多好褥实,那么為了方便解bug呀狼,動態(tài)打日志,并且能夠方便分享發(fā)送日志文件损离,產(chǎn)生了這個demo哥艇。
二、所涉及的技術(shù)NSStream僻澎、NSOutputStream貌踏、NSInputStream 采用數(shù)據(jù)流,動態(tài)把數(shù)據(jù)寫入本地存儲窟勃。UIDocumentInteractionController 采用系統(tǒng)的分享來發(fā)送日志文件祖乳。
三、demo 代碼
3.1JWDOutputInputStream
- (void)doTestInputStream {
[[JWDOutputInputStream shareOutputInputStream] creatInputStreamWithFilePath:@"/Users/jiangweidong/Desktop/Sign.txt"];
}
- (void)doTestOutputStream {
[[JWDOutputInputStream shareOutputInputStream] creatOutputStreamWithFilePath:@"/Users/jiangweidong/Desktop/Sign-test.txt"];
}
上面 doTestInputStream 和 doTestOutputStream 是利用 NSStream 的代理來實現(xiàn) 把一個文件的數(shù)據(jù) 寫入到指定的地址秉氧,
導(dǎo)入 JWDOutputInputStream 類即可使用眷昆。
JWDOutputInputStream.h
#import <Foundation/Foundation.h>
@interface JWDOutputInputStream : NSObject
+ (JWDOutputInputStream *)shareOutputInputStream;
/**
讀取文件
@param filePath 需要讀取文件的地址
*/
- (void)creatInputStreamWithFilePath:(NSString *) filePath;
/**
創(chuàng)建 寫入流
@param filePath 內(nèi)容寫入的文件地址
*/
- (void)creatOutputStreamWithFilePath:(NSString *) filePath;
@end
JWDOutputInputStream.m
#import "JWDOutputInputStream.h"
@interface JWDOutputInputStream ()<NSStreamDelegate>
@property (nonatomic, assign) NSInteger location;//!< <#value#>
@property (nonatomic, strong) NSString *contentFilePath;//!< 讀取內(nèi)容的地址
@end
@implementation JWDOutputInputStream
static JWDOutputInputStream *outputInputStream = nil;
+ (JWDOutputInputStream *)shareOutputInputStream {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
outputInputStream = [[JWDOutputInputStream alloc] init];
});
return outputInputStream;
}
/**
讀取文件
@param filePath 需要讀取文件的地址
*/
- (void)creatInputStreamWithFilePath:(NSString *) filePath {
NSInputStream *readStream = [[NSInputStream alloc]initWithFileAtPath:filePath];
[readStream setDelegate:self];
[readStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[readStream open]; //調(diào)用open開始讀文件
}
#pragma mark -
#pragma mark - 寫
/**
把需要寫入的內(nèi)容轉(zhuǎn)換成 data
@return data
*/
- (NSData *)dataWillWrite{
static NSData *data = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
data = [NSData dataWithContentsOfFile:@"/Users/jiangweidong/Desktop/Sign.txt"];//@"/Users/jiangweidong/Desktop/Sign.txt"
});
return data;
}
/**
創(chuàng)建 寫入流
@param filePath 內(nèi)容寫入的文件地址
*/
- (void)creatOutputStreamWithFilePath:(NSString *) filePath{ // @"/Users/jiangweidong/Desktop/Signstream-write.txt";
NSOutputStream *writeStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:YES];
[writeStream setDelegate:self];
[writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[writeStream open];
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventHasSpaceAvailable: {
NSInteger bufSize = 5;
uint8_t buf[bufSize];
if (self.location > [self dataWillWrite].length) {
[[self dataWillWrite] getBytes:buf range:NSMakeRange(self.location, self.location + bufSize - [self dataWillWrite].length)];
}else if(self.location == [self dataWillWrite].length){
[aStream close];
[[self dataWillWrite] getBytes:buf range:NSMakeRange(self.location, bufSize)];
}else {
[[self dataWillWrite] getBytes:buf range:NSMakeRange(self.location, bufSize)];
}
NSOutputStream *writeStream = (NSOutputStream *)aStream;
[writeStream write:buf maxLength:sizeof(buf)]; //把buffer里的數(shù)據(jù),寫入文件
self.location += bufSize;
if (self.location >= [[self dataWillWrite] length] ) { //寫完后關(guān)閉流
[aStream close];
}
}
break;
case NSStreamEventEndEncountered: {
// 結(jié)束的時候關(guān)閉和一處流操作
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
aStream = nil;
}
break;
//錯誤和無事件處理
case NSStreamEventErrorOccurred:{
}
break;
case NSStreamEventNone:
break;
//打開完成
case NSStreamEventOpenCompleted: {
NSLog(@"NSStreamEventOpenCompleted");
}
break;
default:
break;
}
}
@end
網(wǎng)上這樣使用NSStream 的很多汁咏,這里就不細(xì)致說明亚斋。
3.2 JWDInteractionLogger
重點說一下 JWDInteractionLogger
3.2.1
+(JWDInteractionLogger *)shareInteractionLogger {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
interactionLogger = [[JWDInteractionLogger alloc] init];
});
return interactionLogger;
}
使用單例作為全局統(tǒng)一打日志,只要引入頭文件 即可一句話動態(tài)打日志攘滩,
方式一:宏 引入
/**
日志文件名在這里 統(tǒng)一 標(biāo)記帅刊,作為全局的名稱
*/
#define JWDlogName @"star"
#define JWDINSlogName @"more-star"
/**
只需要引入 宏 傳遞相應(yīng)的參數(shù)
@param logString 需要記錄的日志內(nèi)容
@param fileName 日志文件名
@return 日志宏
*/
#define JWDINSlog(logString,fileName) [[JWDInteractionLogger shareInteractionLogger] writeLogWithLogString:logString withfileName:fileName]
方式二:方法引入
+(JWDInteractionLogger *)shareInteractionLogger;
/**
使用單例 調(diào)用
@param logString 需要記錄的日志內(nèi)容
@param fileName 日志文件名
*/
- (void)writeLogWithLogString:(NSString *)logString withfileName:(NSString *)fileName;
3.2.2
說明 JWDInteractionLogger.m 實現(xiàn)
/**
創(chuàng)建文件路徑
@param fileName 文件名稱
@return 路徑
*/
-(NSURL *)getPathWithFileName:(NSString *)fileName {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *cacheDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
BOOL isDir = TRUE;
BOOL isDirExists = [fileManager fileExistsAtPath:cacheDir isDirectory:&isDir];
if (!isDirExists) {
NSError *err = nil;
BOOL isCreateDirSuccess = [fileManager createDirectoryAtPath:cacheDir withIntermediateDirectories:NO attributes:nil error:&err];
if (!isCreateDirSuccess) {
NSLog(@"創(chuàng)建cache路徑失敗:%@",err.description);
return nil;
}
}
NSString *filePath = [[cacheDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:@"txt"];
BOOL isFileExists = [fileManager fileExistsAtPath:filePath];
if (isFileExists) {
//存在
NSError *err = nil;
float fileSizeKB = [[fileManager attributesOfItemAtPath:filePath error:&err] fileSize] / 1024.0f;
if (err) {
NSLog(@"獲取文件大小失敽洳怠:%@",err.description);
}
if (fileSizeKB > 10240) {
NSError *err = nil;
BOOL isRmSuccess = [fileManager removeItemAtPath:filePath error:&err];
if (!isRmSuccess) {
NSLog(@"刪除文件失敽裰馈:%@",err.description);
}
BOOL isCreateFileSuccess = [fileManager createFileAtPath:filePath contents:nil attributes:nil];
if (!isCreateFileSuccess) {
NSLog(@"創(chuàng)建文件失數茏啤:%@",err.description);
return nil;
}
}
} else {
//不存在
NSError *err = nil;
BOOL isCreateFileSuccess = [fileManager createFileAtPath:filePath contents:nil attributes:nil];
if (!isCreateFileSuccess) {
NSLog(@"創(chuàng)建文件失敗:%@",err.description);
return nil;
}
}
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
if (!fileURL) {
NSLog(@"獲取沙盒相對文件URL失敗");
return nil;
}
return fileURL;
}
上面創(chuàng)建文件路徑冒黑,做了幾種容錯田绑,也對太大的日志文件直接刪除。
3.2.3
/**
創(chuàng)建 流寫 實例
@param fileName 類型名
*/
- (NSOutputStream *)creatStreamWithFileName:(NSString *)fileName {
NSURL *url = [self getPathWithFileName:fileName];
NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:url append:YES];
outputStream.delegate = self;
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[outputStream open];
return outputStream;
}
根據(jù)日志名稱創(chuàng)建寫入流抡爹,做到即時創(chuàng)建掩驱。這里的用法,把流加入到 [NSRunLoop currentRunLoop] 中等待數(shù)據(jù)的寫入冬竟。
3.2.4
- (NSData *)getDataWithLogString:(NSString *)logString {
NSString *tempString = [[NSString alloc] init];
if (logString.length == 0) {
tempString = @"\n";
}else {
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *appName = [infoDictionary objectForKey:@"CFBundleDisplayName"];
NSString *version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
NSDate *date = [NSDate date];
NSDateFormatter *forMatter = [[NSDateFormatter alloc] init];
[forMatter setDateFormat:@"yyyy年MM月dd日 HH時mm分ss秒"];
NSString *dateStr = [forMatter stringFromDate:date];
tempString = [tempString stringByAppendingFormat:@"appName--%@,version--%@,date--%@ ,[文件名:%s]" "[函數(shù)名:%s]" "[行號:%d] \n %@ \n",appName,version,dateStr,[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__,__LINE__,logString];
}
NSData *data = [tempString dataUsingEncoding:NSUTF8StringEncoding];
return data;
}
把需要記錄的日志數(shù)據(jù)轉(zhuǎn)化成NSData欧穴,這里本人拼接進(jìn)去了 APP名,版本號泵殴,日志時間涮帘,文件名,函數(shù)名笑诅,行號调缨,日志數(shù)據(jù)。記錄這些數(shù)據(jù)吆你,做到查看日志的快速定位弦叶,便于查找問題所在,是不是很爽妇多。
3.2.5
- (void)writeLogWithLogString:(NSString *)logString withfileName:(NSString *)fileName{
if (self.writeLogStream == nil) {
self.writeLogStream = [self creatStreamWithFileName:fileName];
}
NSData *data = [self getDataWithLogString:logString];
[self.writeLogStream write:[data bytes] maxLength:data.length];
[self.writeLogStream close];
[self.writeLogStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.writeLogStream = nil;
}
這里就是供外界調(diào)用的日志入口伤哺,數(shù)據(jù)轉(zhuǎn)換,在流對象不存的情況下者祖,做到即時創(chuàng)建立莉,在一條數(shù)據(jù)寫入成功之后,做到即時關(guān)閉數(shù)據(jù)流對象咸包,保護(hù)數(shù)據(jù)不出錯桃序,和 數(shù)據(jù)庫讀寫數(shù)據(jù)地址 一個道理,用時打開烂瘫,不用時及時關(guān)閉媒熊。
四、日志分享
現(xiàn)在本地有了相應(yīng)的日志文件坟比,那么打包安裝的包芦鳍,為了及時方便獲取日志數(shù)據(jù),那么很快的把文件分享發(fā)送出來葛账,豈不更好柠衅。
利用系統(tǒng)的 UIDocumentInteractionController 來實現(xiàn)
- (void)sendLogFileWithFileName:(NSString *)fileName {
NSURL *fileurl = [self getPathWithFileName:fileName];
if (!fileurl) {
NSLog(@"日志文件不存在");
return;
}
// 判斷文件是否存在
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isExists = [fileManager fileExistsAtPath:[fileurl path]];
if (!isExists) {
return;
}
// 獲取文件大小
NSError *error = nil;
CGFloat fileSize = [[fileManager attributesOfItemAtPath:[fileurl path] error:&error] fileSize]/1024.0f;
if (error) {
NSLog(@"獲取日志失敗");
return;
}
if (fileSize == 0) {
NSLog(@"獲取日志失敗,文件不存在");
}else {
self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:fileurl];
self.interactionController.delegate = self;
UIViewController *vc = [UIApplication sharedApplication].keyWindow.rootViewController;
while (vc.presentedViewController) {
vc = vc.presentedViewController;
}
if (vc!=nil) {
[self.interactionController presentOptionsMenuFromRect:vc.view.bounds inView:vc.view animated:YES];
}
}
}
但是當(dāng)我要分享籍琳,使用AirDrop 分享發(fā)送時菲宴,發(fā)送失敗報下面的錯誤贷祈,第三方發(fā)送也是如此,備忘錄里面也是喝峦,顯示文件為空势誊。
后來查閱資料發(fā)現(xiàn),沒有強(qiáng)引用
self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:fileurl];
出現(xiàn)以上的原因是因為沒有全局引用 UIDocumentInteractionController
UIDocumentInteractionController *interactionController = [UIDocumentInteractionController interactionControllerWithURL:fileurl];
導(dǎo)致在分享文件的時候找不到UIDocumentInteractionController谣蠢,獲取文件失敗粟耻。
好了,就說這么多吧眉踱!可以下載demo 自己試一試挤忙,或者放到自己的項目里面試試,當(dāng)你看到日志了是不是感覺感覺很爽谈喳,不用聯(lián)機(jī)就可以分析數(shù)據(jù)册烈,這樣如此好的工具,豈不是和后臺婿禽,服務(wù)端撕逼的神器茄厘。被各種錯誤數(shù)據(jù)虐了千萬遍的客戶端同仁們,這下是不是長嘆一句谈宛,真爽。
當(dāng)測試美美來找你結(jié)束bug時胎署,直接看日志吆录,分析顯示,他媽的數(shù)據(jù)錯誤琼牧,數(shù)據(jù)格式不對恢筝,各種jj,bug 直接拋出去巨坊。當(dāng)然還是要和睦相處撬槽,共同為自家項目加油努力。
如有考慮不周和錯誤的地方歡迎你的指正趾撵,謝謝侄柔!
如果你感覺到,幫助了你占调,就star一個吧暂题!
最后是 demo地址