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

推荐阅读更多精彩内容