当前位置: 移动技术网 > IT编程>移动开发>IOS > IOS本地日志记录解决方案

IOS本地日志记录解决方案

2019年07月24日  | 移动技术网IT编程  | 我要评论

如鹏,苏易正秋佳乙,家中火灾相拥遇难

我们在项目中日志记录这块也算是比较重要的,有时候用户程序出什么问题,光靠服务器的日志还不能准确的找到问题

现在一般记录日志有几种方式:

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(@"首页", @"这是日志信息!",@"可以多参数");

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持移动技术网!

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网