IOS本地日志记录方案

3,771 阅读14分钟
原文链接: www.bbsmax.com

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

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

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 动态库,好下:

由于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 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 - Ini
 
- (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;
             * );
            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 >  ? [dataDic objectForKey:@"data"] : nil;
 
        if([resultDic isEqual:[NSNull null]]){
            error = [NSError errorWithDomain:[NSString stringWithFormat: 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;
    ) {
        // 拉取指定日期的
 
        // "dates": ["2017-03-01", "2017-03-11"]
        NSArray* dates = resultDic[@"dates"];
 
        // 压缩日志
        created = [self compressLog:dates];
    }){
        // 拉取全部
 
        // 压缩日志
        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:];

这里可能有人发现我们在记录日志的时候为什么最后面要加上nil,因为这个是OC中动态参数的结束后缀,不加上nil,程序就不知道你有多少个参数,可能有人又要说了,NSString的 stringWithFormat 方法为什么不需要加 nil 也可以呢,那是因为stringWithFormat里面用到了占位符,就是那些 %@ %i 之类的,这样程序就能判断你有多少个参数了,所以就不用加 nil 了

看到这里,可能大家觉得这个记录日志的方法有点长,后面还加要nil,不方便,那能不能再优化一些,让它更简单的调用呢?我可以用到宏来优化,我们这样定义一个宏,如下:

// 记录本地日志
#define LLog(module,...) [[LogManager sharedInstance] logInfo:module logStr:__VA_ARGS__,nil]

这样我们使用的时候就方便了,这样调用就行了。

LLog(@"首页", @"这是日志信息!",@"可以多参数");

好的,那本文就结束了,这也是将我工作中用的的分享给大家,老鸟就可以飞过了~~有什么看不明白的就留言吧。

IOS本地日志记录方案的更多相关文章

  1. IOS本地日志记录解决方案

    我们在项目中日志记录这块也算是比较重要的,有时候用户程序出什么问题,光靠服务器的日志还不能准确的找到问题 现在一般记录日志有几种方式: 1.使用第三方工具来记录日志,如腾讯的Bugly,它是只把程序的 ...

  2. IOS异常日志记录与展现功能

    在平常的APP开发过程中经常碰到程序遇到异常闪退的问题,通过日志可以把相关的详细错误信息进行记录,本实例要记录不管在哪个页面出错都要进行记录,这边使用到的日志记录插件CocoaLumberjack,以 ...

  3. C# 面向切面编程--监控日志记录方案

    背景:现在公司整体在做监控平台,要求把各个部分的细节都记录下来,在前台页面上有所显示,所以现在需要做的就是一个监控日志的记录工作,今天讲的就是渲染监控日志的例子. 现状:当前的渲染程序没有为监控日志记 ...

  4. iOS崩溃日志记录工具--CrashlyTics

    http://try.crashlytics.com Crashlytics优势: 1.Crashlytics基本不会漏掉任何应用崩溃的信息 2.Crashlytics对崩溃日志管理很人性化,会根据崩 ...

  5. docker容器日志收集方案(方案二 filebeat+syslog本地日志收集)

    与方案一一样都是把日志输出到本地文件系统使用filebeat进行扫描采集 不同的是输出的位置是不一样的 我们对docker进行如下设置 sudo docker service update  --lo ...

  6. docker容器日志收集方案(方案一 filebeat+本地日志收集)

    filebeat不用多说就是扫描本地磁盘日志文件,读取文件内容然后远程传输. docker容器日志默认记录方式为 json-file 就是将日志以json格式记录在磁盘上 格式如下: { " ...

  7. C#中四步轻松使用log4net记录本地日志

    在这里,记录我在项目中使用log4net记录本地日志的步骤.在不会之前感觉很难,很神秘,一旦会了之后其实没那么难.其实所有的事情都是一样的,下面我就分享一下我使用log4Net的经验. 第一步:首先从 ...

  8. C#中四步轻松使用log4net记录本地日志(WPF有点小区别)

    在这里,记录我在项目中使用log4net记录本地日志的步骤.在不会之前感觉很难,很神秘,一旦会了之后其实没那么难.其实所有的事情都是一样的,下面我就分享一下我使用log4Net的经验. 第一步:首先从 ...

  9. Java日志记录的事儿

    一.java日志组件 1.common-logging common-logging是apache提供的一个通用的日志接口.用户可以自由选择第三方的日志组件作为具体实现,像log4j,或者jdk自带的 ...

随机推荐

  1. Chrome Dev Tools :成为更高效的开发人员

    原文出处 http://blog.jobbole.com/22065/ 实时CSS Style编辑 选择一个Dom,可以对Dom进行编辑和操作,实时修改Css Style, 同时CssStyle可以保 ...

  2. [Java面试十二]数据库概念相关

    1. 什么是存储过程?它有什么优点? 答:存储过程是一组予编译的SQL语句,它的优点有:     允许模块化程序设计,就是说只需要创建一次过程,以后在程序中就可以调用该过程任意次.     允许更快执 ...

  3. 引用参数,值参数,ref,out

    1,一个参数只有在引用的时候才能改变其值,这是一种情况 2,一个参数在引用后要永久的改变其值(可以用返回参数的形式) 3,多个参数在引用后要永久的改变其值或者多个参数中的部分(返回参数就适合了,因为只 ...

  4. 最全面的jdbcUtils,总有一种适合你

    附加jar包,TxQueryRunner.java文件,dbconfig.properties配置文件(点击链接下载): http://files.cnblogs.com/files/xiaoming ...

  5. shell 进程查询相关的命令

    同一个进程部署多个,如何知道删除哪个: ll /proc/ 会显示位置 查看pid下哪些socket链接 ll /proc//fd

  6. SQL中的日期时间函数

    之所以把日期时间函数单独拿出来回顾一下,是因为这一部分的内容比较独立,C#中也有类似的日期时间函数,趁着想得起来,写个标题先.

  7. Godaddy主机从购买到开通的详细图文教程(2013年)

    http://bbs.zhujiusa.com/thread-10-1-1.html Godaddy主机从购买到开通的详细图文教程(2013年最新) Godaddy是全球域名注册商中的NO.1,同时也 ...

  8. AjaxHelper的get和post请求的封装类

    最近在学习ajax的时候发现不断的调用get和post请求时,代码重复很多,有的公司会用自带的封装的方法,这个可以直接调用ajax的方法,但是在运用的时候我们也应该学习它是怎么封装的和一些原理性的东西 ...

  9. Java经典案例之-“成绩等级划分”

    /** * 描述:在对话框中输入学习成绩,利用条件运算符的嵌套来完成此题:学习成绩>=90分的同学用A表示,60-89分之间的用B表示, * 60分以下的用C表示. * 分析:可以利用三目运算符 ...

  10. 数据结构与算法 —— 链表linked list(04)

    我们在上篇文章里面提到了链表的翻转,给定一个链表,对每两个相邻的节点作交换,并返回头节点,今天的这道题是它的升级版,如下: k个一组翻转链表 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链 ...