IOS本地日志記錄解決方案
我們?cè)陧?xiàng)目中日志記錄這塊也算是比較重要的,有時(shí)候用戶(hù)程序出什么問(wèn)題,光靠服務(wù)器的日志還不能準(zhǔn)確的找到問(wèn)題
現(xiàn)在一般記錄日志有幾種方式:
1、使用第三方工具來(lái)記錄日志,如騰訊的Bugly,它是只把程序的異常日志,程序崩潰日志,以及一些自定義的操作日志上傳到Bugly的后臺(tái)
2、我們把日志記錄到本地,在適合的時(shí)候再上傳到服務(wù)器
這里我要介紹的是第二種方法,第一種和第二種可以一起用。
假如現(xiàn)在有下面這樣的日志記錄要求
1、日志記錄在本地
2、日志最多記錄N天,N天之前的都需要清理掉
3、日志可以上傳到服務(wù)器,由服務(wù)器控制是否需要上傳
4、上傳的日志應(yīng)該壓縮后再上傳
實(shí)現(xiàn)思路
1、日志記錄在本地
也就是把字符串保存到本地,我們可以用 將NSString轉(zhuǎn)換成NSData然后寫(xiě)入本地,但是NSData寫(xiě)入本地會(huì)對(duì)本地的文件進(jìn)入覆蓋,所以我們只有當(dāng)文件不存在的時(shí)候第一次寫(xiě)入的時(shí)候用這種方式,如果要將日志內(nèi)容追加到日志文件里面,我們可以用NSFleHandle來(lái)處理
2、日志最多記錄N天,N天之前的都需要清理掉
這個(gè)就比較容易了,我們可以將本地日志文件名定成當(dāng)天日期,每天一個(gè)日志文件,這樣我們?cè)诔绦騿?dòng)后,可以去檢測(cè)并清理掉過(guò)期的日志文件
3、日志可以上傳到服務(wù)器,由服務(wù)器控制是否需要上傳
這個(gè)功能我們需要后臺(tái)的配合,后臺(tái)需要提供兩個(gè)接口,一個(gè)是APP去請(qǐng)求時(shí)返回當(dāng)前應(yīng)用是否需要上傳日志,根據(jù)參數(shù)來(lái)判斷,第二個(gè)接口就是上傳日志的接口
4、上傳的日志應(yīng)該壓縮后再上傳
一般壓縮的功能我們可以使用zip壓縮,OC中有開(kāi)源的插件 ZipArchive 地址:http://code.google.com/p/ziparchive/ (需要FQ)
具體實(shí)現(xiàn)代碼
我們先將ZipArchive引入到項(xiàng)目中,注意還需要引入系統(tǒng)的 libz.tbd 動(dòng)態(tài)庫(kù),如下:


由于ZipArchive是使用C++編寫(xiě)的,是不支持ARC的,所以我們需要在項(xiàng)目中把這個(gè)類(lèi)的ARC關(guān)閉掉,不然會(huì)編譯不通過(guò),如下:

給ZipArchive.mm文件添加一個(gè) -fno-objc-arc 標(biāo)簽就可以了
然后就是代碼部分了,創(chuàng)建一個(gè)日志工具類(lèi),LogManager
// // LogManager.h // LogFileDemo // // Created by xgao on 17/3/9. // Copyright © 2017年 xgao. All rights reserved. // #import <Foundation/Foundation.h> @interface LogManager : NSObject /** * 獲取單例實(shí)例 * * @return 單例實(shí)例 */ + (instancetype) sharedInstance; #pragma mark - Method /** * 寫(xiě)入日志 * * @param module 模塊名稱(chēng) * @param logStr 日志信息,動(dòng)態(tài)參數(shù) */ - (void)logInfo:(NSString*)module logStr:(NSString*)logStr, ...; /** * 清空過(guò)期的日志 */ - (void)clearExpiredLog; /** * 檢測(cè)日志是否需要上傳 */ - (void)checkLogNeedUpload; @end
//
// LogManager.m
// LogFileDemo
//
// Created by xgao on 17/3/9.
// Copyright © 2017年 xgao. All rights reserved.
//
#import "LogManager.h"
#import "ZipArchive.h"
#import "XGNetworking.h"
// 日志保留最大天數(shù)
static const int LogMaxSaveDay = 7;
// 日志文件保存目錄
static const NSString* LogFilePath = @"/Documents/OTKLog/";
// 日志壓縮包文件名
static NSString* ZipFileName = @"OTKLog.zip";
@interface LogManager()
// 日期格式化
@property (nonatomic,retain) NSDateFormatter* dateFormatter;
// 時(shí)間格式化
@property (nonatomic,retain) NSDateFormatter* timeFormatter;
// 日志的目錄路徑
@property (nonatomic,copy) NSString* basePath;
@end
@implementation LogManager
/**
* 獲取單例實(shí)例
*
* @return 單例實(shí)例
*/
+ (instancetype) sharedInstance{
static LogManager* instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!instance) {
instance = [[LogManager alloc]init];
}
});
return instance;
}
// 獲取當(dāng)前時(shí)間
+ (NSDate*)getCurrDate{
NSDate *date = [NSDate date];
NSTimeZone *zone = [NSTimeZone systemTimeZone];
NSInteger interval = [zone secondsFromGMTForDate: date];
NSDate *localeDate = [date dateByAddingTimeInterval: interval];
return localeDate;
}
#pragma mark - Init
- (instancetype)init{
self = [super init];
if (self) {
// 創(chuàng)建日期格式化
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:@"yyyy-MM-dd"];
// 設(shè)置時(shí)區(qū),解決8小時(shí)
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
self.dateFormatter = dateFormatter;
// 創(chuàng)建時(shí)間格式化
NSDateFormatter* timeFormatter = [[NSDateFormatter alloc]init];
[timeFormatter setDateFormat:@"HH:mm:ss"];
[timeFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
self.timeFormatter = timeFormatter;
// 日志的目錄路徑
self.basePath = [NSString stringWithFormat:@"%@%@",NSHomeDirectory(),LogFilePath];
}
return self;
}
#pragma mark - Method
/**
* 寫(xiě)入日志
*
* @param module 模塊名稱(chēng)
* @param logStr 日志信息,動(dòng)態(tài)參數(shù)
*/
- (void)logInfo:(NSString*)module logStr:(NSString*)logStr, ...{
#pragma mark - 獲取參數(shù)
NSMutableString* parmaStr = [NSMutableString string];
// 聲明一個(gè)參數(shù)指針
va_list paramList;
// 獲取參數(shù)地址,將paramList指向logStr
va_start(paramList, logStr);
id arg = logStr;
@try {
// 遍歷參數(shù)列表
while (arg) {
[parmaStr appendString:arg];
// 指向下一個(gè)參數(shù),后面是參數(shù)類(lèi)似
arg = va_arg(paramList, NSString*);
}
} @catch (NSException *exception) {
[parmaStr appendString:@"【記錄日志異?!?];
} @finally {
// 將參數(shù)列表指針置空
va_end(paramList);
}
#pragma mark - 寫(xiě)入日志
// 異步執(zhí)行
dispatch_async(dispatch_queue_create("writeLog", nil), ^{
// 獲取當(dāng)前日期做為文件名
NSString* fileName = [self.dateFormatter stringFromDate:[NSDate date]];
NSString* filePath = [NSString stringWithFormat:@"%@%@",self.basePath,fileName];
// [時(shí)間]-[模塊]-日志內(nèi)容
NSString* timeStr = [self.timeFormatter stringFromDate:[LogManager getCurrDate]];
NSString* writeStr = [NSString stringWithFormat:@"[%@]-[%@]-%@\n",timeStr,module,parmaStr];
// 寫(xiě)入數(shù)據(jù)
[self writeFile:filePath stringData:writeStr];
NSLog(@"寫(xiě)入日志:%@",filePath);
});
}
/**
* 清空過(guò)期的日志
*/
- (void)clearExpiredLog{
// 獲取日志目錄下的所有文件
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.basePath error:nil];
for (NSString* file in files) {
NSDate* date = [self.dateFormatter dateFromString:file];
if (date) {
NSTimeInterval oldTime = [date timeIntervalSince1970];
NSTimeInterval currTime = [[LogManager getCurrDate] timeIntervalSince1970];
NSTimeInterval second = currTime - oldTime;
int day = (int)second / (24 * 3600);
if (day >= LogMaxSaveDay) {
// 刪除該文件
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@/%@",self.basePath,file] error:nil];
NSLog(@"[%@]日志文件已被刪除!",file);
}
}
}
}
/**
* 檢測(cè)日志是否需要上傳
*/
- (void)checkLogNeedUpload{
__block NSError* error = nil;
// 獲取實(shí)體字典
__block NSDictionary* resultDic = nil;
// 請(qǐng)求的URL,后臺(tái)功能需要自己做
NSString* url = [NSString stringWithFormat:@"%@/common/phone/logs",SERVIERURL];
// 發(fā)起請(qǐng)求,從服務(wù)器上獲取當(dāng)前應(yīng)用是否需要上傳日志
[[XGNetworking sharedInstance] get:url success:^(NSString* jsonData) {
// 獲取實(shí)體字典
NSDictionary* dataDic = [Utilities getDataString:jsonData error:&error];
resultDic = dataDic.count > 0 ? [dataDic objectForKey:@"data"] : nil;
if([resultDic isEqual:[NSNull null]]){
error = [NSError errorWithDomain:[NSString stringWithFormat:@"請(qǐng)求失敗,data沒(méi)有數(shù)據(jù)!"] code:500 userInfo:nil];
}
// 完成后的處理
if (error == nil) {
// 處理上傳日志
[self uploadLog:resultDic];
}else{
LOGERROR(@"檢測(cè)日志返回結(jié)果有誤!data沒(méi)有數(shù)據(jù)!");
}
} faild:^(NSString *errorInfo) {
LOGERROR(([NSString stringWithFormat:@"檢測(cè)日志失?。?@",errorInfo]));
}];
}
#pragma mark - Private
/**
* 處理是否需要上傳日志
*
* @param resultDic 包含獲取日期的字典
*/
- (void)uploadLog:(NSDictionary*)resultDic{
if (!resultDic) {
return;
}
// 0不拉取,1拉取N天,2拉取全部
int type = [resultDic[@"type"] intValue];
// 壓縮文件是否創(chuàng)建成功
BOOL created = NO;
if (type == 1) {
// 拉取指定日期的
// "dates": ["2017-03-01", "2017-03-11"]
NSArray* dates = resultDic[@"dates"];
// 壓縮日志
created = [self compressLog:dates];
}else if(type == 2){
// 拉取全部
// 壓縮日志
created = [self compressLog:nil];
}
if (created) {
// 上傳
[self uploadLogToServer:^(BOOL boolValue) {
if (boolValue) {
LOGINFO(@"日志上傳成功---->>");
// 刪除日志壓縮文件
[self deleteZipFile];
}else{
LOGERROR(@"日志上傳失?。?!");
}
} errorBlock:^(NSString *errorInfo) {
LOGERROR(([NSString stringWithFormat:@"日志上傳失敗??!Error:%@",errorInfo]));
}];
}
}
/**
* 壓縮日志
*
* @param dates 日期時(shí)間段,空代表全部
*
* @return 執(zhí)行結(jié)果
*/
- (BOOL)compressLog:(NSArray*)dates{
// 先清理幾天前的日志
[self clearExpiredLog];
// 獲取日志目錄下的所有文件
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.basePath error:nil];
// 壓縮包文件路徑
NSString * zipFile = [self.basePath stringByAppendingString:ZipFileName] ;
ZipArchive* zip = [[ZipArchive alloc] init];
// 創(chuàng)建一個(gè)zip包
BOOL created = [zip CreateZipFile2:zipFile];
if (!created) {
// 關(guān)閉文件
[zip CloseZipFile2];
return NO;
}
if (dates) {
// 拉取指定日期的
for (NSString* fileName in files) {
if ([dates containsObject:fileName]) {
// 將要被壓縮的文件
NSString *file = [self.basePath stringByAppendingString:fileName];
// 判斷文件是否存在
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
// 將日志添加到zip包中
[zip addFileToZip:file newname:fileName];
}
}
}
}else{
// 全部
for (NSString* fileName in files) {
// 將要被壓縮的文件
NSString *file = [self.basePath stringByAppendingString:fileName];
// 判斷文件是否存在
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
// 將日志添加到zip包中
[zip addFileToZip:file newname:fileName];
}
}
}
// 關(guān)閉文件
[zip CloseZipFile2];
return YES;
}
/**
* 上傳日志到服務(wù)器
*
* @param returnBlock 成功回調(diào)
* @param errorBlock 失敗回調(diào)
*/
- (void)uploadLogToServer:(BoolBlock)returnBlock errorBlock:(ErrorBlock)errorBlock{
__block NSError* error = nil;
// 獲取實(shí)體字典
__block NSDictionary* resultDic;
// 訪問(wèn)URL
NSString* url = [NSString stringWithFormat:@"%@/fileupload/fileupload/logs",SERVIERURL_FILE];
// 發(fā)起請(qǐng)求,這里是上傳日志到服務(wù)器,后臺(tái)功能需要自己做
[[XGNetworking sharedInstance] upload:url fileData:nil fileName:ZipFileName mimeType:@"application/zip" parameters:nil success:^(NSString *jsonData) {
// 獲取實(shí)體字典
resultDic = [Utilities getDataString:jsonData error:&error];
// 完成后的處理
if (error == nil) {
// 回調(diào)返回?cái)?shù)據(jù)
returnBlock([resultDic[@"state"] boolValue]);
}else{
if (errorBlock){
errorBlock(error.domain);
}
}
} faild:^(NSString *errorInfo) {
returnBlock(errorInfo);
}];
}
/**
* 刪除日志壓縮文件
*/
- (void)deleteZipFile{
NSString* zipFilePath = [self.basePath stringByAppendingString:ZipFileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:zipFilePath]) {
[[NSFileManager defaultManager] removeItemAtPath:zipFilePath error:nil];
}
}
/**
* 寫(xiě)入字符串到指定文件,默認(rèn)追加內(nèi)容
*
* @param filePath 文件路徑
* @param stringData 待寫(xiě)入的字符串
*/
- (void)writeFile:(NSString*)filePath stringData:(NSString*)stringData{
// 待寫(xiě)入的數(shù)據(jù)
NSData* writeData = [stringData dataUsingEncoding:NSUTF8StringEncoding];
// NSFileManager 用于處理文件
BOOL createPathOk = YES;
if (![[NSFileManager defaultManager] fileExistsAtPath:[filePath stringByDeletingLastPathComponent] isDirectory:&createPathOk]) {
// 目錄不存先創(chuàng)建
[[NSFileManager defaultManager] createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
}
if(![[NSFileManager defaultManager] fileExistsAtPath:filePath]){
// 文件不存在,直接創(chuàng)建文件并寫(xiě)入
[writeData writeToFile:filePath atomically:NO];
}else{
// NSFileHandle 用于處理文件內(nèi)容
// 讀取文件到上下文,并且是更新模式
NSFileHandle* fileHandler = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
// 跳到文件末尾
[fileHandler seekToEndOfFile];
// 追加數(shù)據(jù)
[fileHandler writeData:writeData];
// 關(guān)閉文件
[fileHandler closeFile];
}
}
@end
日志工具的使用
1、記錄日志
[[LogManager sharedInstance] logInfo:@"首頁(yè)" logStr:@"這是日志信息!",@"可以多參數(shù)",nil];
2、我們?cè)诔绦騿?dòng)后,進(jìn)行一次檢測(cè),看要不要上傳日志
// 幾秒后檢測(cè)是否有需要上傳的日志 [[LogManager sharedInstance] performSelector:@selector(checkLogNeedUpload) withObject:nil afterDelay:3];
這里可能有人發(fā)現(xiàn)我們?cè)谟涗浫罩镜臅r(shí)候?yàn)槭裁醋詈竺嬉由蟦il,因?yàn)檫@個(gè)是OC中動(dòng)態(tài)參數(shù)的結(jié)束后綴,不加上nil,程序就不知道你有多少個(gè)參數(shù),可能有人又要說(shuō)了,NSString的 stringWithFormat 方法為什么不需要加 nil 也可以呢,那是因?yàn)閟tringWithFormat里面用到了占位符,就是那些 %@ %i 之類(lèi)的,這樣程序就能判斷你有多少個(gè)參數(shù)了,所以就不用加 nil 了
看到這里,可能大家覺(jué)得這個(gè)記錄日志的方法有點(diǎn)長(zhǎng),后面還加要nil,不方便,那能不能再優(yōu)化一些,讓它更簡(jiǎn)單的調(diào)用呢?我可以用到宏來(lái)優(yōu)化,我們這樣定義一個(gè)宏,如下:
// 記錄本地日志 #define LLog(module,...) [[LogManager sharedInstance] logInfo:module logStr:__VA_ARGS__,nil]
這樣我們使用的時(shí)候就方便了,這樣調(diào)用就行了。
LLog(@"首頁(yè)", @"這是日志信息!",@"可以多參數(shù)");
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持腳本之家!
相關(guān)文章
iOS組件封裝與自動(dòng)布局自定義表情鍵盤(pán)
這篇文章主要介紹了iOS組件封裝與自動(dòng)布局自定義表情鍵盤(pán)的相關(guān)資料,需要的朋友可以參考下2016-04-04
實(shí)例解析iOS應(yīng)用多線程開(kāi)發(fā)中NSthread類(lèi)的用法
這篇文章主要介紹了iOS應(yīng)用多線程開(kāi)發(fā)中NSthread類(lèi)的用法,代碼基于傳統(tǒng)的Objective-C,NSthread類(lèi)需要的朋友可以參考下2016-02-02
iOS開(kāi)發(fā)生成二維碼圖片(附中間帶有小圖標(biāo)二維碼)
這篇文章主要介紹了iOS開(kāi)發(fā)生成二維碼圖片(附中間帶有小圖標(biāo)二維碼)的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-10-10
IOS實(shí)現(xiàn)自定義透明背景的tabbar
這篇文章介紹的是在IOS中怎樣把tabbar背景設(shè)置為透明,有需要的小伙伴們可以參考借鑒。2016-08-08
詳解IOS的Automatically Sign在設(shè)備上打包
本篇教程主要給大家分享了IOS的Automatically Sign如何在設(shè)備上直接打包,有需要的朋友參考學(xué)習(xí)下。2018-01-01
iOS runtime forwardInvocation詳解及整理
這篇文章主要介紹了 iOS runtime forwardInvocation詳解及整理的相關(guān)資料,需要的朋友可以參考下2017-02-02

