IOS本地日志记录解决方案
我们在项目中日志记录这块也算是比较重要的,有时候用户程序出什么问题,光靠服务器的日志还不能准确的找到问题
现在一般记录日志有几种方式:
1、使用第三方工具来记录日志,如腾讯的bugly,它是只把程序的异常日志,程序崩溃日志,以及一些自定义的操作日志上传到bugly的后台
2、我们把日志记录到本地,在适合的时候再上传到服务器
这里我要介绍的是第二种方法,第一种和第二种可以一起用。
假如现在有下面这样的日志记录要求
1、日志记录在本地
2、日志最多记录n天,n天之前的都需要清理掉
3、日志可以上传到服务器,由服务器控制是否需要上传
4、上传的日志应该压缩后再上传
实现思路
1、日志记录在本地
也就是把字符串保存到本地,我们可以用 将nsstring转换成nsdata然后写入本地,但是nsdata写入本地会对本地的文件进入覆盖,所以我们只有当文件不存在的时候第一次写入的时候用这种方式,如果要将日志内容追加到日志文件里面,我们可以用nsflehandle来处理
2、日志最多记录n天,n天之前的都需要清理掉
这个就比较容易了,我们可以将本地日志文件名定成当天日期,每天一个日志文件,这样我们在程序启动后,可以去检测并清理掉过期的日志文件
3、日志可以上传到服务器,由服务器控制是否需要上传
这个功能我们需要后台的配合,后台需要提供两个接口,一个是app去请求时返回当前应用是否需要上传日志,根据参数来判断,第二个接口就是上传日志的接口
4、上传的日志应该压缩后再上传
一般压缩的功能我们可以使用zip压缩,oc中有开源的插件 ziparchive 地址: (需要fq)
具体实现代码
我们先将ziparchive引入到项目中,注意还需要引入系统的 libz.tbd 动态库,如下:
由于ziparchive是使用c++编写的,是不支持arc的,所以我们需要在项目中把这个类的arc关闭掉,不然会编译不通过,如下:
给ziparchive.mm文件添加一个 -fno-objc-arc 标签就可以了
然后就是代码部分了,创建一个日志工具类,logmanager
// // logmanager.h // logfiledemo // // created by xgao on 17/3/9. // copyright © 2017年 xgao. all rights reserved. // #import <foundation/foundation.h> @interface logmanager : nsobject /** * 获取单例实例 * * @return 单例实例 */ + (instancetype) sharedinstance; #pragma mark - method /** * 写入日志 * * @param module 模块名称 * @param logstr 日志信息,动态参数 */ - (void)loginfo:(nsstring*)module logstr:(nsstring*)logstr, ...; /** * 清空过期的日志 */ - (void)clearexpiredlog; /** * 检测日志是否需要上传 */ - (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" // 日志保留最大天数 static const int logmaxsaveday = 7; // 日志文件保存目录 static const nsstring* logfilepath = @"/documents/otklog/"; // 日志压缩包文件名 static nsstring* zipfilename = @"otklog.zip"; @interface logmanager() // 日期格式化 @property (nonatomic,retain) nsdateformatter* dateformatter; // 时间格式化 @property (nonatomic,retain) nsdateformatter* timeformatter; // 日志的目录路径 @property (nonatomic,copy) nsstring* basepath; @end @implementation logmanager /** * 获取单例实例 * * @return 单例实例 */ + (instancetype) sharedinstance{ static logmanager* instance = nil; static dispatch_once_t oncetoken; dispatch_once(&oncetoken, ^{ if (!instance) { instance = [[logmanager alloc]init]; } }); return instance; } // 获取当前时间 + (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) { // 创建日期格式化 nsdateformatter* dateformatter = [[nsdateformatter alloc]init]; [dateformatter setdateformat:@"yyyy-mm-dd"]; // 设置时区,解决8小时 [dateformatter settimezone:[nstimezone timezonewithabbreviation:@"utc"]]; self.dateformatter = dateformatter; // 创建时间格式化 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 /** * 写入日志 * * @param module 模块名称 * @param logstr 日志信息,动态参数 */ - (void)loginfo:(nsstring*)module logstr:(nsstring*)logstr, ...{ #pragma mark - 获取参数 nsmutablestring* parmastr = [nsmutablestring string]; // 声明一个参数指针 va_list paramlist; // 获取参数地址,将paramlist指向logstr va_start(paramlist, logstr); id arg = logstr; @try { // 遍历参数列表 while (arg) { [parmastr appendstring:arg]; // 指向下一个参数,后面是参数类似 arg = va_arg(paramlist, nsstring*); } } @catch (nsexception *exception) { [parmastr appendstring:@"【记录日志异常】"]; } @finally { // 将参数列表指针置空 va_end(paramlist); } #pragma mark - 写入日志 // 异步执行 dispatch_async(dispatch_queue_create("writelog", nil), ^{ // 获取当前日期做为文件名 nsstring* filename = [self.dateformatter stringfromdate:[nsdate date]]; nsstring* filepath = [nsstring stringwithformat:@"%@%@",self.basepath,filename]; // [时间]-[模块]-日志内容 nsstring* timestr = [self.timeformatter stringfromdate:[logmanager getcurrdate]]; nsstring* writestr = [nsstring stringwithformat:@"[%@]-[%@]-%@\n",timestr,module,parmastr]; // 写入数据 [self writefile:filepath stringdata:writestr]; nslog(@"写入日志:%@",filepath); }); } /** * 清空过期的日志 */ - (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); } } } } /** * 检测日志是否需要上传 */ - (void)checklogneedupload{ __block nserror* error = nil; // 获取实体字典 __block nsdictionary* resultdic = nil; // 请求的url,后台功能需要自己做 nsstring* url = [nsstring stringwithformat:@"%@/common/phone/logs",servierurl]; // 发起请求,从服务器上获取当前应用是否需要上传日志 [[xgnetworking sharedinstance] get:url success:^(nsstring* jsondata) { // 获取实体字典 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:@"请求失败,data没有数据!"] code:500 userinfo:nil]; } // 完成后的处理 if (error == nil) { // 处理上传日志 [self uploadlog:resultdic]; }else{ logerror(@"检测日志返回结果有误!data没有数据!"); } } faild:^(nsstring *errorinfo) { logerror(([nsstring stringwithformat:@"检测日志失败!%@",errorinfo])); }]; } #pragma mark - private /** * 处理是否需要上传日志 * * @param resultdic 包含获取日期的字典 */ - (void)uploadlog:(nsdictionary*)resultdic{ if (!resultdic) { return; } // 0不拉取,1拉取n天,2拉取全部 int type = [resultdic[@"type"] intvalue]; // 压缩文件是否创建成功 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 日期时间段,空代表全部 * * @return 执行结果 */ - (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]; // 创建一个zip包 bool created = [zip createzipfile2:zipfile]; if (!created) { // 关闭文件 [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]; } } } // 关闭文件 [zip closezipfile2]; return yes; } /** * 上传日志到服务器 * * @param returnblock 成功回调 * @param errorblock 失败回调 */ - (void)uploadlogtoserver:(boolblock)returnblock errorblock:(errorblock)errorblock{ __block nserror* error = nil; // 获取实体字典 __block nsdictionary* resultdic; // 访问url nsstring* url = [nsstring stringwithformat:@"%@/fileupload/fileupload/logs",servierurl_file]; // 发起请求,这里是上传日志到服务器,后台功能需要自己做 [[xgnetworking sharedinstance] upload:url filedata:nil filename:zipfilename mimetype:@"application/zip" parameters:nil success:^(nsstring *jsondata) { // 获取实体字典 resultdic = [utilities getdatastring:jsondata error:&error]; // 完成后的处理 if (error == nil) { // 回调返回数据 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]; } } /** * 写入字符串到指定文件,默认追加内容 * * @param filepath 文件路径 * @param stringdata 待写入的字符串 */ - (void)writefile:(nsstring*)filepath stringdata:(nsstring*)stringdata{ // 待写入的数据 nsdata* writedata = [stringdata datausingencoding:nsutf8stringencoding]; // nsfilemanager 用于处理文件 bool createpathok = yes; if (![[nsfilemanager defaultmanager] fileexistsatpath:[filepath stringbydeletinglastpathcomponent] isdirectory:&createpathok]) { // 目录不存先创建 [[nsfilemanager defaultmanager] createdirectoryatpath:[filepath stringbydeletinglastpathcomponent] withintermediatedirectories:yes attributes:nil error:nil]; } if(![[nsfilemanager defaultmanager] fileexistsatpath:filepath]){ // 文件不存在,直接创建文件并写入 [writedata writetofile:filepath atomically:no]; }else{ // nsfilehandle 用于处理文件内容 // 读取文件到上下文,并且是更新模式 nsfilehandle* filehandler = [nsfilehandle filehandleforupdatingatpath:filepath]; // 跳到文件末尾 [filehandler seektoendoffile]; // 追加数据 [filehandler writedata:writedata]; // 关闭文件 [filehandler closefile]; } } @end
日志工具的使用
1、记录日志
[[logmanager sharedinstance] loginfo:@"首页" logstr:@"这是日志信息!",@"可以多参数",nil];
2、我们在程序启动后,进行一次检测,看要不要上传日志
// 几秒后检测是否有需要上传的日志 [[logmanager sharedinstance] performselector:@selector(checklogneedupload) withobject:nil afterdelay:3];
这里可能有人发现我们在记录日志的时候为什么最后面要加上nil,因为这个是oc中动态参数的结束后缀,不加上nil,程序就不知道你有多少个参数,可能有人又要说了,nsstring的 stringwithformat 方法为什么不需要加 nil 也可以呢,那是因为stringwithformat里面用到了占位符,就是那些 %@ %i 之类的,这样程序就能判断你有多少个参数了,所以就不用加 nil 了
看到这里,可能大家觉得这个记录日志的方法有点长,后面还加要nil,不方便,那能不能再优化一些,让它更简单的调用呢?我可以用到宏来优化,我们这样定义一个宏,如下:
// 记录本地日志 #define llog(module,...) [[logmanager sharedinstance] loginfo:module logstr:__va_args__,nil]
这样我们使用的时候就方便了,这样调用就行了。
llog(@"首页", @"这是日志信息!",@"可以多参数");
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!