CocoaLumberjack框架学习

关于日志系统

CocoaLumberjack是一个iOS/OSX 下的一个日志系统,据说比原生的NSLog要快很多倍,CocoaLumberjack是Mac和iOS上一个集快捷、简单、强大和灵活于一身的日志框架。CocoaLumberjack类似于流行的日志框架(如log4j),但它是专为Objective-C设计的,利用了多线程、GCD(如果可用)、无锁原子操作Objective-C运行时的动态特性。我这段时间正好要用上,走读了一下代码,仅作记录。

简单介绍

CocoaLumberjack是一个扩展性较好的一个日志开源框架,以总DDLog为对面接口,内部采用了基于不同的流向的logger来拆分具体的实现,用户可以仅仅使用其中某一个logger来记录,也可以多个logger同时记录,甚至自己注册一个logger来达到自己的目的,达到记录日志的功能。

基本框架

@ DDLog(基础框架)

@ DDASLLogger(发送到苹果的日志系统,他们显示到控制台)

@DDTTYLoyger (发送日志语句到控制台)将 log 发送给 Xcode 的控制台

@DDFIleLoger (把输出信息写进文件中)将 log 写入本地文件

这3个是日志的输出流器,用户可以自行决定是否需要注册这些,个人认为DDASLLogger是没有必要的,因为这个实际上输出到苹果的/var/log/messagelog下面去的,所以这个可以不注册

所有的log都通过DDLog进行分发,在DDLog这里已经将整个message日志进行了打包,并且区分同步异步,然后丢到DDLog下的GCD队列中,队列会将日志丢到各个解析器中去解析。每个 Logger 处理收到的 log 也是在它们自己的 GCD队列下(loggingQueue)做的,它们询问其下的 Formatter,获取 Log 消息格式,然后最终根据 Logger 完成最终的输出。

配置

第一件事情你要做的是在你applicationDidFinishLaunching方法中注册你所需要的日志组件,通用配置是:

 [DDLog addLogger:[DDASLLogger sharedInstance]]; [DDLog addLogger:[DDTTYLogger sharedInstance]];

至于DDFileLogger,这个需要配置其超时刷入的时间,形成一个文件的时间等。需要配置文件路径需要继承DDLogFileManagerDefault,然后在documentsFileManager配置路径。若要修改文件名,则需要在BaseLogFileManager里面复写父类的- (BOOL)isLogFile:(NSString *)fileName;-(NSString *)newLogFileName等方法。

//这里BaseLogFileManager继承DDLogFileManagerDefault
 BaseLogFileManager *documentsFileManager = [[BaseLogFileManager alloc] init];
 _fileLogger = [[DDFileLogger alloc] initWithLogFileManager:documentsFileManager];
 _fileLogger.rollingFrequency = 24 * 60 * 60; // 24 hour 将文件打包,再开写下一个文件
 _fileLogger.logFileManager.maximumNumberOfLogFiles = 7;//保留七天的日志
 [DDLog addLogger:_fileLogger];

第二件事是配置全局日志等级:

RO_LOG_LEVEL_OFF

RO_LOG_LEVEL_ERROR

RO_LOG_LEVEL_WARN

RO_LOG_LEVEL_INFO

RO_LOG_LEVEL_DEBUG

RO_LOG_LEVEL_VERBOSE

等级从下至上越来越严格,使用:

RLogLevel = RO_LOG_LEVEL_INFO 即可全局配置。

使用

使用接口比较简单,将NSLog替换为如下几个等级接口即可:

RLogError

RLogWarn

RLogInfo

RLogDebug

RLogVerbose

如:

NSLog(@”App Crashed, name : %@ “, name);

替换为:

RLogInfo(@”App Crashed, name : %@ “, name);

代码走读

经过宏解析到

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag 

在其中涉及到第一个同步,即加队列同步,在工程中有一个同步队列,长度1000,使用信号量来控制,若超出队列则卡住当前调用日志的线程,直到可以加入队列为止。

在这里做了这么几件事,一个是讲日志入DDLog的队列,而这个队列长度有限,默认1000,这里做了一个信号量,主要是为了保证队列最大数不会被超出。一般情况下1000条日志确实不会在同一时间出现,而且只有ERROR等级的日志才会被同步输出,其他等级的日志异步输出。我测试了一下,将队列改小,且换成ERROR等级日志,那么如果超出的数过大,确实会造成当前的打印日志的线程阻塞。

如下代码做了修改,测试极端情况

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag {

    static int i = 0;
    i++;
    NSLog(@"111 ************ %@ %d",_queueSemaphore,i);
    dispatch_semaphore_wait(_queueSemaphore, DISPATCH_TIME_FOREVER);

    // We've now sure we won't overflow the queue.
    // It is time to queue our log message.
    NSLog(@"222 ************ %@ %d",_queueSemaphore,i);
    dispatch_block_t logBlock = ^{
        @autoreleasepool {
            NSLog(@"wode xiancheng shi  %@",[NSThread currentThread]);
            sleep(2);
            [self lt_log:logMessage];
        }
    };

    if (asyncFlag) {
        dispatch_async(_loggingQueue, logBlock);
        
    } else {
        dispatch_sync(_loggingQueue, logBlock);
        
    }
    NSLog(@"333 ************ %@ %d",_queueSemaphore,i);
}

2016-06-23 11:38:02.808 RuiFuTech[26157:1191052] 111 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 5

2016-06-23 11:38:02.809 RuiFuTech[26157:1191052] 222 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 5

2016-06-23 11:38:02.809 RuiFuTech[26157:1191052] 333 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 5

2016-06-23 11:38:02.809 RuiFuTech[26157:1191052] 111 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 6

2016-06-23 11:38:02.805 [ReOrient]: ———————– first ———————

2016-06-23 11:38:04.815 RuiFuTech[26157:1191052] 222 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 6

2016-06-23 11:38:04.815 RuiFuTech[26157:1191089] wode xiancheng shi <NSThread: 0x7ff449c1c7f0>{number = 2, name = (null)}

2016-06-23 11:38:04.816 RuiFuTech[26157:1191052] 333 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 6

2016-06-23 11:38:04.816 RuiFuTech[26157:1191052] 111 **** <OS_dispatch_semaphore: 0x7ff449f2dcd0> 7

分析: 队列总数为5,第6条日志被卡住2秒钟,直到第一个日志输出成功后,第6条日志加入队列,第七条日志再被卡住。注意这里是DDLog用的是串行队列。看来作者是笃定了后续各个解析器logger够快,而且不会有我这样的测试。

在下一层:

- (void)lt_log:(DDLogMessage *)logMessage

这里判断多核处理器后,遍历之前注册的loggers,判断message的等级是否达到logger的输出等级,并且控制第二次同步,即当你注册多个Logger的时候,使用GCD中的group来控制同步返回,确保这个事件的多个Logger都成功后才退出。

 if (!(logMessage->_flag & loggerNode->_level)) {
                continue;
 }

说到这里,有必要将这个枚举贴出来: 我们一般用位移来做这个,但是这样写,简直是清晰明了高大上。

typedef NS_ENUM(NSUInteger, DDLogLevel){
    /**
     *  No logs
     */
    DDLogLevelOff       = 0,
    
    /**
     *  Error logs only        0x1
     */
    DDLogLevelError     = (DDLogFlagError),     
    
    /**
     *  Error and warning logs   0x11
     */
    DDLogLevelWarning   = (DDLogLevelError   | DDLogFlagWarning),
    
    /**
     *  Error, warning and info logs 0x111
     */
    DDLogLevelInfo      = (DDLogLevelWarning | DDLogFlagInfo),
    
    /**
     *  Error, warning, info and debug logs 
     */
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug),
    
    /**
     *  Error, warning, info, debug and verbose logs
     */
    DDLogLevelVerbose   = (DDLogLevelDebug   | DDLogFlagVerbose),
    
    /**
     *  All logs (1...11111)
     */
    DDLogLevelAll       = NSUIntegerMax
};

之后使用GCD的group做同步,即要所有的logger都输出完这个message后才能退出这个队列。

dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
                [loggerNode->_logger logMessage:logMessage];
            } });
   dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);

如果在DDFileLogger里面- (void)logMessage:(DDLogMessage *)logMessage 加入延时,发现调用全部都被卡住,这里group会等待全部输出完成后才进行下一步,所以我在想,如果是高并发的日志输出的话,File的输出肯定是要慢于控制台的,那么IO高的情况下,全部日志都会被一点点的占满之前的全局GCD同步队列,最后队列满了之后再卡住调用日志的线程。不过这个情况实在是不大可能。

- (void)logMessage:(DDLogMessage *)logMessage

实际上是重载了DDAbstractLogger的方法,其实这里我们自己要写一个logger,同样继承这个就好了。

现在着重看一下DDFileLogger,我们需要做两个事情,一个是定制合适的文件名,一个是修改log存放路径。这里要注意一个类_logFileManager

@interface DDFileLogger () {
    __strong id <DDLogFileManager> _logFileManager;
    
    DDLogFileInfo *_currentLogFileInfo;
    NSFileHandle *_currentLogFileHandle;
    
    dispatch_source_t _currentLogFileVnode;
    dispatch_source_t _rollingTimer;
    
    unsigned long long _maximumFileSize;
    NSTimeInterval _rollingFrequency;
}

对文件的存储,名字的自定义需要继承DDLogFileManager,在CocoaLumberjack的实现里,发现其又封装了一个接口DDLogFileManagerDefault,那么我们想要对文件管控就继承DDLogFileManagerDefault就可以了。继承后主要复写如下两个函数:

-(NSString *)newLogFileName 
- (BOOL)isLogFile:(NSString *)fileName

我在Stack Overflow看到回答是需要将isLogFile返回NO来确保每次创建新文件,但是仔细一想发现不对,如果app在较短时间内重启两次,那么第二次的日志是需要接在上一次的日志末尾的,而不是每次都要创建一个新的文件。而创建文件的时机完全可以通过自身的rolling来解决。按照这个思路再看了一遍isLogFile的相关代码,发现这里是对格式校验,发现是符合自己格式的文件则算是找到,在其末尾加上日志。

那么我也在这里复写这个方法,另外在复写logFileDateFormatter函数,从而达到目的。

想到这里就想看一下他是怎么找自身日志文件的,原来以为会出去文件名的时间戳,然后减去一个rolling的周期,来判断要不要继续写下去。但是测试了一下发现不对,它是按照文件的创建时间来断定的,我创建了一个文件aa,然后将isLogFile返回YES,居然在aa里打日志了。。。。。

touch -t 200910112200 filename 这里创建的文件名即使是当前时间,也不会添加到这里,原因是创建日期被修改到2009年。

另外这一块代码在unsortedLogFileInfos里面有介绍,具体就不仔细看了。重点看下DDFileLogger,可以看下currentLogFileHandle和maybeRollLogFileDueToSize,了解文件的切分和定时任务的指派。差不多就是这些。

另外对于DDLoggerNode,其实可以认为是对DDLogger的一层封装,这个可以过一下

实际使用的时候发现了不少不爽的地方

1.在Debug的时候,如果单步调试,则会出现运行过这个日志的时候不打印日志,原因是异步输出,另一个线程会在合适的时候输出(DDFileLogger,DDASLLogger,@DDTTYLoyger内部做了同步,只要一个没结束都还要等待),这里考虑到方便性,我在这段代码里做了同步,这样就会等待这行日志输出后才会走到下一步了。(影响性能,但是毕竟是Debug等级)

#define DDLogDebug(frmt, ...)   LOG_MAYBE(NO,                RLogLevel, RO_LOG_FLAG_DEBUG,   RO_LOG_CONTEXT, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)

2.在默认情况下,使用RLogError会同步输出日志,使用其他等级的接口将异步输出日志。如果单步调试,异步日志是不会立即输出的。若需要同步输出,在头文件中也可以做修改。

3.代码中Debug以及Verbose会打印出行号,函数名等信息,具体格式为

 FileName | FunName @LineNumber

例:

2016-06-27 16:22:04.218 [XXX]: FileLogPath = 1
2016-06-27 16:23:04.632 [XXX]: Manager | -[Manager initLogger] @74 | FileLogPath = 1
最近的文章

IOS下的图片模糊化模糊化

起因最近在模仿苹果端网易云音乐客户端,在进入音乐那里特别喜欢图片毛玻璃的效果,这个看了下网上,有开源的设计,但是封装的很蛋疼,而且最恶心的是,为啥接口和实现不分开呢。基于这点,我对实现进行了封装。接口初始化并设置蒙版的颜色-(ANBlurredImageView *)initWithBlurAmount:(CGFloat)amount withTintColor:(UIColor *)color; 默认的模糊化设置-(void)blurredImageViewDefault; 默认的清晰化...…

继续阅读
更早的文章

基于树莓派的微信服务器

微信公众平台首先要去了解这个东西,去看官方的手册及博客,网上面关于微信公众号的后台接入有三个解决办法, 使用微信推荐的后端平台,这个到公众号里面去找就能看到,这个相当于腾讯找人代理帮你搞定后端了。 使用sae和gae这样的平台来部署自己的应用程序。 使用自己的服务器,公有云独立ip。这三个办法都可以,网上介绍的一堆一堆的,在国内购买公有云手续比较复杂,我推荐用digitalocean,国外的服务器,推荐新加坡部署的服务器,使用我的推荐链接,注册成功后有10刀的返现,[点击这里](ht...…

继续阅读