首页 > 系统 > iOS > 正文

IOS本地日志记录解决方案

2019-10-21 18:47:17
字体:
来源:转载
供稿:网友

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

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

1、使用第三方工具来记录日志,如腾讯的Bugly,它是只把程序的异常日志,程序崩溃日志,以及一些自定义的操作日志上传到Bugly的后台

2、我们把日志记录到本地,在适合的时候再上传到服务器

这里我要介绍的是第二种方法,第一种和第二种可以一起用。

假如现在有下面这样的日志记录要求

1、日志记录在本地

2、日志最多记录N天,N天之前的都需要清理掉

3、日志可以上传到服务器,由服务器控制是否需要上传

4、上传的日志应该压缩后再上传

实现思路

1、日志记录在本地

也就是把字符串保存到本地,我们可以用 将NSString转换成NSData然后写入本地,但是NSData写入本地会对本地的文件进入覆盖,所以我们只有当文件不存在的时候第一次写入的时候用这种方式,如果要将日志内容追加到日志文件里面,我们可以用NSFleHandle来处理

2、日志最多记录N天,N天之前的都需要清理掉

这个就比较容易了,我们可以将本地日志文件名定成当天日期,每天一个日志文件,这样我们在程序启动后,可以去检测并清理掉过期的日志文件

3、日志可以上传到服务器,由服务器控制是否需要上传

这个功能我们需要后台的配合,后台需要提供两个接口,一个是APP去请求时返回当前应用是否需要上传日志,根据参数来判断,第二个接口就是上传日志的接口

4、上传的日志应该压缩后再上传

一般压缩的功能我们可以使用zip压缩,OC中有开源的插件 ZipArchive 地址:http://code.google.com/p/ziparchive/ (需要FQ)

具体实现代码

我们先将ZipArchive引入到项目中,注意还需要引入系统的 libz.tbd 动态库,如下:

ios,记录日志,记录崩溃日志,日志解决方案

ios,记录日志,记录崩溃日志,日志解决方案

由于ZipArchive是使用C++编写的,是不支持ARC的,所以我们需要在项目中把这个类的ARC关闭掉,不然会编译不通过,如下:

ios,记录日志,记录崩溃日志,日志解决方案

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

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


注:相关教程知识阅读请移步到IOS开发频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表