IOS本地日誌記錄方案

来源:http://www.cnblogs.com/xgao/archive/2017/03/15/6553334.html
-Advertisement-
Play Games

我們在項目中日誌記錄這塊也算是比較重要的,有時候用戶程式出什麼問題,光靠伺服器的日誌還不能準確的找到問題。 現在一般記錄日誌有幾種方式: 1、使用第三方工具來記錄日誌,如騰訊的Bugly,它是只把程式的異常日誌,程式崩潰日誌,以及一些自定義的操作日誌上傳到Bugly的後臺 2、我們把日誌記錄到本地, ...


我們在項目中日誌記錄這塊也算是比較重要的,有時候用戶程式出什麼問題,光靠伺服器的日誌還不能準確的找到問題。

現在一般記錄日誌有幾種方式:

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,不方便,那能不能再優化一些,讓它更簡單的調用呢?我可以用到巨集來優化,我們這樣定義一個巨集,如下:

// 記錄本地日誌
#define LLog(module,...) [[LogManager sharedInstance] logInfo:module logStr:__VA_ARGS__,nil]

這樣我們使用的時候就方便了,這樣調用就行了。

LLog(@"首頁", @"這是日誌信息!",@"可以多參數");

好的,那本文就結束了,這也是將我工作中用的的分享給大家,老鳥就可以飛過了~~有什麼看不明白的就留言吧。

 

 

 

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 給大家介紹的是原生js更改CSS樣式的兩種方式: 1通過target.style.cssText="CSS表達式"實現 2活用classname動態更改樣式 ...
  • 任何一個容器都可以指定為Flex佈局。 行內元素也可以使用Flex佈局。 Webkit內核的瀏覽器,必須加上-webkit首碼。 註意,設為Flex佈局以後,子元素的float、clear和vertical-align屬性將失效。 採用Flex佈局的元素,稱為Flex容器(flex containe ...
  • 這是《前端總結·基礎篇·JS》系列的第二篇,主要總結一下JS數組的使用、技巧以及常用方法。 ...
  • 簡單的express restful設計以及實現(一) 代碼地址為: "github" 主要是漫長的填坑之旅,涉及到的內容有node express restful ,雖然沒有完全的但是也是要記錄下來, npm install npm start 這裡解釋下 'npm start'就是運行packa ...
  • 雖然這種看完,你會感覺真的很low,但是,並不是沒有什麼用處,這種書寫格式可以用在次導航的製作上以及網站首頁的footer部分,甚至也可以用這種格式做二級三級頁面,總之,做主導航的時候是不能使用的,因為這會造成用戶體驗感覺不好,而且,如果遇到大神級人物看見這樣的網站導航,會降低對你這個公司的評價和信 ...
  • 題目描述 獲取 url 中的參數 1. 指定參數名稱,返回該參數的值 或者 空字元串 2. 不指定參數名稱,返回全部的參數對象 或者 {} 3. 如果存在多個同名參數,則返回數組 輸入例子: 輸出例子: 方法 思路其實都差不多: 1. 匹配出key=value中的key和value; 2. 需要返回 ...
  • @charset "utf-8"; html{ color:#000; background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,form,fieldset,input,textarea,p,blockquote,th,td { margin:0; p ...
  • UITableView的預設的cell的分割線左邊沒有頂滿,而右邊卻頂滿了。這樣顯示很難看。我需要讓其左右兩邊都是未頂滿狀態,距離是20像素 添加UITableView的一個代理方法: ode1處代碼: 定製cell分割線的frame code2處代碼: -layoutMargins returns ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...