iOS实录9:iOS开发中的日志工具迭代

[这是第9篇]

导语: 日志输出不仅仅是NSLog的简单使用,它对定位开发中的问题,收集用户的使用习惯有着很重要的作用。下面根据在项目中的实践,介绍我在iOS开发中对日志工具的迭代过程。

一、日志工具V1.0####

1、日志工具V1.0的主要目标#####
  • 直接使用NSLog打印的信息,比较单一,没有函数信息、没有代码行数等,在调试的时候。希望打印的信息“丰富些”;
  • 在真机上,为了摒弃NSLog的输出对系统资源的消耗,和避免私密数据的泄漏,在正式发布中,将输出全部摒弃掉(就是什么都不输出)
2、日志工具V1.0的实现#####

在项目的pch文件中,定义如下:
#if DEBUG
#define QSLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
#else
#define QSLOG(fmt,...) {}
#endif

  • __PRETTY_FUNCTION__是函数相关信息的宏

  • __LINE__是当前源代码行号的宏

  • VA_ARGS 是C99编译器标准中定义的一个可变参数宏(variadic macros)。宏前面加上##的作用在于,当可变参数的个数为0时,##可以把前面多余的","去掉。如果有可变参数, 编译器会把这些可变参数放到逗号的后面。

  • #if DEBUG中这个DEBUG 是在 **"Target > Build Settings > Preprocessor Macros > Debug"中定义的(默认定义的)。保证了条件编译的"#if"可以编译。这就导致了在QSLOG在开发环境下打印日志,而在发布环境下,不打印任何内容(这恰恰是我们的目的)。

3、日志工具V1.0的使用#####
QSLOG(@"嘻嘻嘻");
QSLOG(@"嘻嘻嘻%@嘻嘻嘻", @"哈哈哈");   //含参数

对应的输出是:

2017-05-02 16:06:50.098872 QSUseLogUtilDemo[276:28201] -[ViewController viewDidLoad] [Line 57] 嘻嘻嘻
2017-05-02 16:06:50.098932 QSUseLogUtilDemo[276:28201] -[ViewController viewDidLoad] [Line 58] 嘻嘻嘻哈哈哈嘻嘻嘻
4、日志工具V1.0的不足#####
  • 日志工具V1.0没有提供日志的保存。
  • 在新版本内部灰度的时候,真机上发生某些问题,因为没有保存日志输出,所以定位问题比较困难;尤其是某些特定真机上出问题,而其他真机不出问题,排查问题更加困难,几乎靠猜和祈祷。

二、日志工具V2.0####

1、日志工具V2.0的主要目标#####
  • 在日志工具V1.0的基础上,允许将日志信息保存到文件中。
  • 非release版本在连接Xcode的情况下,输出日志到Xcode控制台,否则输出到日志文件。
2、日志工具V2.0的实现#####

1)在项目的pch文件中,定义如下(和日志工具V1.0一样)
#if DEBUG
#define QSLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
#else
#define QSLOG(fmt,...) {}
#endif

2)定义QSOldLogUtil类,日志工具类

//QSOldLogUtil.h
@interface QSOldLogUtil : NSObject

+ (void)openRedirectLogToDoc;

+ (NSString *)logContent;

@end

//QSOldLogUtil.m
@implementation QSOldLogUtil
+ (void)openRedirectLogToDoc{

    NSString *logFilePath = [self logFilePath];
    NSData *data = [NSData dataWithContentsOfFile:logFilePath];
    if ([data length] > 1000 * 1000) {
        //文件大小超过1MB,删除文件(iphone中文件大小进制1000,不是1024)
        [[NSFileManager defaultManager] removeItemAtPath:logFilePath error:nil];
    }

    //标准输出文件stdout,标准错误输出文件stderr
    freopen([logFilePath cStringUsingEncoding:NSUTF8StringEncoding], "a+", stdout);
    freopen([logFilePath cStringUsingEncoding:NSUTF8StringEncoding], "a+", stderr);
    QSLOG(@"\n\n********************************\n\n");
}

+ (NSString *)logContent{

    NSError *error;
    NSString *content = [NSString stringWithContentsOfFile:[self logFilePath] encoding:NSUTF8StringEncoding error:&error];
    if (!error) {
        return content;
    }
    return error.description;
}

#pragma mark - private methods
+ (NSString *)logFilePath{

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docDir = [paths objectAtIndex:0];
    NSString *fileName = [NSString stringWithFormat:@"appName-log.log"];
    return [docDir stringByAppendingPathComponent:fileName];
}
@end

** 3)在AppDelegate中添加如下代码**

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    BOOL isConnectToXcode = isatty(STDOUT_FILENO);
    if(!isConnectToXcode) {
        //isConnectToXcode为NO,认为设备没有连接Xcode,调试日志重定向输出到文件
        [QSOldLogUtil openRedirectLogToDoc];
   }

   // ...
   return YES;
}

:我们在真机没有连接Xcode的情形下,将日志保存在文件中,日志文件大小限制是1MB,(iphone中文件大小进制1000,不是1024),超过1MB就删除后,从头开始写,否则接着上次的地方写。

3、日志工具V2.0的使用#####
QSLOG(@"嘻嘻嘻");
QSLOG(@"嘻嘻嘻%@嘻嘻嘻", @"哈哈哈");   //含参数

非release版本下,真机连接Xcode情况下,日志输出到Xcode控制台;不连接Xcode情况下,日志输出到文件。输出格式和日志工具V1.0的输出格式一样(QSLog的宏没有改变)

3、日志工具V2.0的不足#####

日志工具V2.0存在时间很短,是一个极其短暂的过度版本。主要原因有两个:

  • 认识到CocoaLumberjack的强大
  • Xcode 8的坑:Xcode 8默认情况下输出的OS_ACTIVITY_MODE log,造成输出日志很乱,我禁用了OS_ACTIVITY_MODE log 禁用方法Product -> Scheme -> Edit Scheme -> Environment Varibales 添加字段OS_ACTIVITY_MODE 并将属性值设置为 disable**】,但是也造成iOS 10真机连接Xcode调试的时候,QSLOG不会输出日志,因为NSLog被屏蔽掉了。

三、日志工具V3.0####

1、日志工具V3.0的主要目标#####
  • 虽然CocoaLumberjack框架很完善,但是考虑到项目中已经集成了极其完善的异常上报和埋点统计等工具,日志工具V3.0只需要去记录和保存调试日志输出信息(当然也可以用它上报日志到后台等等)。
  • 考虑到完全习惯使用QSLOG,所以日志工具的实现细节尽可能对其他开发人员透明,无感知。保证大家还是继续愉快地使用QSLOG,此外,还可以很方便地查看日志信息。
2、日志工具V3.0的实现#####

日志工具V3.0是基于CocoaLumberjack库实现的,主要实现的功能有:

  • 能够指定日志的格式,非release版本可以将日志显示到控制台和保存到文件,release版本没有任何输出
  • 解决了(避开了)禁用了**OS_ACTIVITY_MODE log ** 带来iOS 10真机调试没有日志输出的bug
  • 提供显示日志的视图,开发人员可以很方便地该View。查看最新的日志信息。
  • 为了充分利用日志的视图的空间,在视图上还可以查看屏幕帧数

1) 通过cocoapods导入CocoaLumberjack,在Podfile中添加

target "QSUseLogUtilDemo" do
  # 其他第三方库....
  pod 'CocoaLumberjack'
end

2) 在pch文件中设置全局ddLogLevel,修改QSLog的宏
#ifdef DEBUG
static const long ddLogLevel = DDLogLevelDebug;
#else
static const long ddLogLevel = DDLogLevelOff; //无日志
#endif

#define QSLOG(fmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, fmt, ##__VA_ARGS__);

说明1:DDLogFlag是一次log的等级, 而DDLogLevel是log输出等级, 如果一次log的等级,低于这个Logger的level,就不会打log.

typedef NS_OPTIONS(NSUInteger, DDLogFlag){
    DDLogFlagError      = (1 << 0),
    DDLogFlagWarning    = (1 << 1),
    DDLogFlagInfo       = (1 << 2),
    DDLogFlagDebug      = (1 << 3),
    DDLogFlagVerbose    = (1 << 4)
};

typedef NS_ENUM(NSUInteger, DDLogLevel){
    DDLogLevelOff       = 0,
    DDLogLevelError     = (DDLogFlagError),
    DDLogLevelWarning   = (DDLogLevelError   | DDLogFlagWarning),
    DDLogLevelInfo      = (DDLogLevelWarning | DDLogFlagInfo),
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug),
    DDLogLevelVerbose   = (DDLogLevelDebug   | DDLogFlagVerbose),
    DDLogLevelAll       = NSUIntegerMax
};

说明2: ddLogLevel很重要,指定日志的显示类型(LogLevel 用来过滤每条Log,低于LogLevel等级,不会被输出),在release情况下,ddLogLevel 为DDLogLevelOff,不会有日志输出;在非release情况下,QSLOG使用的日志级别是DDLogFlagDebug,达到ddLogLevel 指定的DDLogLevelDebug输出等级,可以被输出。因此,在非release情况下NSLOG是真正输出的,在release情况下NSLOG是没有实际输出的。

3)输入日志格式类QSLogFormatter (只展示重要代码)
实现DDLogFormatter接口协议,指定日志输出格式

  //指定日志输出格式
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage{

    NSString *timeStr = [self.dateFormatter stringFromDate:logMessage.timestamp];
    NSString *flagStr = QSLogFlagToString(logMessage.flag);
    NSString *formatStr = [NSString stringWithFormat:@"%@ %@ %@ %@ line:%ld %@ %@",timeStr,flagStr,logMessage.queueLabel,logMessage.fileName,(long)logMessage.line,logMessage.function,logMessage.message];
    return formatStr;
}
@end

4)日志工具类QSLogUtil类(只展示重要代码)

+ (void)setupConfig{

    //添加控制台输出Logger
    [[DDTTYLogger sharedInstance] setLogFormatter:[[QSLogFormatter alloc] init]];
    [DDLog addLogger:[DDTTYLogger sharedInstance] withLevel:ddLogLevel]; // TTY = Xcode console
//  [DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs

    #if DEBUG
    fileLogger = [[DDFileLogger alloc] init];
    fileLogger.logFormatter = [[QSLogFormatter alloc] init];
    fileLogger.rollingFrequency = 0;
    fileLogger.maximumFileSize = 1000 * 1000;  //限制1MB
    [DDLog addLogger:fileLogger withLevel:ddLogLevel];   //日志文件
    QSLOG(@"********************************\n");
#endif
}

+ (NSString *)logContent{
    return [NSString stringWithContentsOfFile:[self logFilePath] encoding:NSUTF8StringEncoding error:nil];
}

+ (NSString *)logFilePath {
    return [[fileLogger currentLogFileInfo] filePath];
}

4)日志视图显示类QSLogView类(只展示重要代码)

+ (void)show{

    QSLogView *logView = [[QSLogView alloc]init];
    NSArray *rootVCViewSubViews = [[UIApplication sharedApplication].delegate window].rootViewController.view.subviews;
    for (UIView *logView in rootVCViewSubViews) {
        if ([logView isKindOfClass:[QSLogView class]]) {
            return;
        }
    }
    [[((NSObject <UIApplicationDelegate> *)([UIApplication sharedApplication].delegate)) window].rootViewController.view addSubview:logView];
}

+ (void)close {

    NSArray *rootVCViewSubViews=[[UIApplication sharedApplication].delegate window].rootViewController.view.subviews;
    for (QSLogView *view in rootVCViewSubViews) {
        if ([view isKindOfClass:[QSLogView class]]) {
            QSLogView *logView = (QSLogView *)view;
            [logView removeFromSuperview];
        }
    }
}

//更新屏幕帧数
- (void)timerFire:(CADisplayLink *)link{

    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }

    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;

    NSString *fpsString = [NSString stringWithFormat:@"[%d FPS]",(int)round(fps)];
    if (!self.showLogBtn.hidden) {
        [self.showLogBtn setTitle:[NSString stringWithFormat:@"[显示]%@",fpsString] forState:UIControlStateNormal];
    }

    if (!self.closeLogBtn.hidden) {
        [self.closeLogBtn setTitle:[NSString stringWithFormat:@"[关闭]%@",fpsString] forState:UIControlStateNormal];
    }
}
3、日志工具V3.0的使用#####

1)为了在DEBUG环境下开启日志记录和日志显示View,需要在appDelegate中添加如下代码

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    #if DEBUG
        [QSLogUtil openLog];  //开启日志记录
    #endif

    //self.window的初始化和指定rootViewController

    [self.window makeKeyWindow];

#if DEBUG
    [QSLogView show];   //显示日志记录视图
#endif

    return YES;
}

2)平时的输出日志还是使用QSLOG,和以前没有任何区别

QSLOG(@"嘻嘻嘻");
QSLOG(@"嘻嘻嘻%@嘻嘻嘻", @"哈哈哈");   //含参数
4、日志工具V3.0使用的效果图 (DEBUG模式下,Release不会有日志输出)#####

1)日志记录视图入口开启

  • 红色框显示屏幕帧数,通过点击红色框中的按钮,可以显示保存在机器中的日志内容,和控制台中一样。
  • 也可以通过点击中部的黄色区域,模拟日志输出和反复开启和关闭QSLogView
日志记录视图入口开启.png

此时的控制台输出内容是:
控制台输出日志内容.png

2)QSLogView显示日志

  • 红色框显示屏幕帧数,通过点击红色框中的按钮,可以关闭显示日志记录


    QSLogView显示日志.png

源码直通车QSUseLogUtilDemo
可以的话,欢迎star

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

推荐阅读更多精彩内容