ios 本地日志, 日志上传服务器

原文地址: http://www.cnblogs.com/xgao/p/6553334.html

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

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

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 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,不方便,那能不能再优化一些,让它更简单的调用呢?我可以用到宏来优化,我们这样定义一个宏,如下:

// 记录本地日志

#defineLLog(module,...) [[LogManager sharedInstance] logInfo:module logStr:__VA_ARGS__,nil]

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容