看了几篇文章,一开始感觉整合第三方的日志库CocoaLumberjack
比较简单,不过真正落地,发现还是有几个地方需要注意的。
下载代码
CocoaLumberjack/CocoaLumberjack
- 日志库
CocoaLumberjack
目前已经到了3.0.0
版本,既支持Object-C
,也支持Swift
。包管理方面,既支持CocoaPods
,也支持Carthage
。根据当前工程现状,选择Object-C
+CocoaPods
的方式 - 在原有的Podfile中添加一行
pod 'CocoaLumberjack', '~> 3.0.0'
然后,将终端切换到工程目录,执行命令。带上--no-repo-update
是为了加快更新速度
pod install --no-repo-update
基本需求
- 可以设定
Log
等级 - 可以积攒到一定量的
log
后,一次性发送给服务器,绝对不能打一个Log
就发一次 - 可以一定时间后,将未发送的
log
发送到服务器 - 可以在
App
切入后台时将未发送的log
发送到服务器
利用 CocoaLumberjack 搭建自己的 Log 系统
这篇文章写得很有代表性,这次也主要是按照这个来做。
接口设计
- 一般的文章,都介绍在
APPdelegate
中添加代码,这会导致这个类很乱,不是很好 - 一版本的工程,都有自己的前缀,在工程里到处使用
DDLog
和整体氛围不搭调,最好在中间再包一层。 - 由于是日志,大家都习惯了
NSLog(frmt,...)
这种可变参数形式的c
风格调用,而且一般还是宏定义的方式。
引入一个单独的类,采用(类方法 + 单例)的模式,简化接口,保证只执行一次
- 日志是一种服务,所以,文件命名为
XXXLogService
,作为一个中间隔离层。用户不需要知道日志系统是怎么实现的,用了哪个第三方库 - 提供一个类方法,将初始化的代码放在里面,在APPdelegate中只要一句调用就可以了,比如
[XXXLogService start];
- 以下是接口头文件
XXXLogService.h
的内容,将这个头文件加入pch
文件中,就可以在工程里方便使用XXXLog()
了
#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
// 默认的宏,方便使用
#define XXXLog(frmt, ...) XXXLogInfo(frmt, ...)
// 提供不同的宏,对应到特定参数的对外接口
#define XXXLogError(frmt, ...) DDLogError(frmt, ##__VA_ARGS__)
#define XXXLogWarning(frmt, ...) DDLogWarn(frmt, ##__VA_ARGS__)
#define XXXLogInfo(frmt, ...) DDLogInfo(frmt, ##__VA_ARGS__)
#define XXXLogDebug(frmt, ...) DDLogDebug(frmt, ##__VA_ARGS__)
#define XXXLogVerbose(frmt, ...) DDLogVerbose(frmt, ##__VA_ARGS__)
@interface XXXLogService : NSObject
+ (void)start;
@end
- 以下是接口实现文件
XXXLogService.m
的内容,对外的类接口start
只是一层封装。具体的实现在一个成员函数中,这里用了单例,将只执行一次的内容放在了init
函数中。
这个类只是一层封装,没有做具体的事情。
内容是从网上抄的,这里准备自定义一个loger
往后台传日志,下面那么注释为没必要的内容,实际使用时就直接删除了。至于颜色控件相关设置,其实也没有必要,这里只是做个备忘。
#import "XXXLogService.h"
#import "XXXLogger.h"
#import "XXXLogFormatter.h"
@implementation XXXLogService
+ (void)start {
[self sharedInstance];
}
+ (instancetype)sharedInstance{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
// 自定义的log,要传自己的后台
XXXLogger *logger = [[XXXLogger alloc] init];
XXXLogFormatter *formatter = [[XXXLogFormatter alloc] init];
[logger setLogFormatter:formatter];
[DDLog addLogger:logger];
// 注释中一些不需要的代码只是放在这里,见证一下历史;正式使用时都应该删除。
// XCode的log
// XCode8之后不支持插件工作,所以这些设置颜色的代码不需要,否则将会有无用的颜色信息混入log
//开启使用 XcodeColors
setenv("XcodeColors", "YES", 0);
//检测
char *xcode_colors = getenv("XcodeColors");
if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) {
// XcodeColors is installed and enabled!
NSLog(@"XcodeColors is installed and enabled");
//开启DDLog 颜色
[[DDTTYLogger sharedInstance] setColorsEnabled:YES];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor lightGrayColor] backgroundColor:nil forFlag:DDLogFlagVerbose];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor grayColor] backgroundColor:nil forFlag:DDLogFlagDebug];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor blueColor] backgroundColor:nil forFlag:DDLogFlagInfo];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor yellowColor] backgroundColor:nil forFlag:DDLogFlagWarning];
[[DDTTYLogger sharedInstance] setForegroundColor:[UIColor redColor] backgroundColor:nil forFlag:DDLogFlagError];
}
// XCode的log,也用自定义的输出格式
[[DDTTYLogger sharedInstance] setLogFormatter:formatter];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
// DDASLLogger是输出到mac终端,没有必要再手机上用
[DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs
// DDFileLogger是存在手机上,在Cache目录,一般拿不出来,所以一般也没什么大用
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
[DDLog addLogger:fileLogger];
}
return self;
}
- 删除无用代码,精简过后的
init
代码如下
- (instancetype)init {
self = [super init];
if (self) {
// 自定义的log,要传自己的后台
XXXLogger *logger = [[WJSLogger alloc] init];
XXXLogFormatter *formatter = [[WJSLogFormatter alloc] init];
[logger setLogFormatter:formatter];
[DDLog addLogger:logger];
// XCode的log,也用自定义的输出格式
[[DDTTYLogger sharedInstance] setLogFormatter:formatter];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
}
return self;
}
日志级别
- 需要用一个静态全局变量来定义日志级别
- 日志级别是用来控制日志输出的
- 通过日志
flag
(前面定义的)和日志级别level
(这里定义的)比较,决定是否输出日志 - 日志级别
Error
最高Verbose
最低,flag > level
就输出,否则就不输出
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif
这样定义的结果是:
DEBUG
模式:所有的日志都输出
其他模式:仅仅DDLogError()
和 DDLogWarn()
输出,其他的都没有输出
- 这个变量的名字ddLogLevel最好命名为
ddLogLevel
,不然编译不通过。原因是里面的宏定义用到了。
#ifndef LOG_LEVEL_DEF
#define LOG_LEVEL_DEF ddLogLevel
#endif
- 结合前面的宏定义,提供的方便方法,可以做到开发环境日志很全,而正式环境只搜集
warning
和error
两种日志 - 在实现文件统一定义,使用者不需要知道日志级别
level
这个概念,使用变得更简单。
// 默认的宏,方便使用
#define XXXLog(frmt, ...) XXXLogInfo(frmt, ...)
自定义格式
- 如果需要自定义协议格式,那么需要实现
DDLogFormatter
协议的方法- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;
/**
* This protocol describes the behavior of a log formatter
*/
@protocol DDLogFormatter <NSObject>
@required
/**
* Formatters may optionally be added to any logger.
* This allows for increased flexibility in the logging environment.
* For example, log messages for log files may be formatted differently than log messages for the console.
*
* For more information about formatters, see the "Custom Formatters" page:
* Documentation/CustomFormatters.md
*
* The formatter may also optionally filter the log message by returning nil,
* in which case the logger will not log the message.
**/
- (NSString * __nullable)formatLogMessage:(DDLogMessage *)logMessage;
@end
- 下面是
DDLogMessage
的定义,使用时一般用指针符号`->``引用内部变量,而给出的属性都是可读的,不能访问。不是非常理解这种设计意图。
/**
* The `DDLogMessage` class encapsulates information about the log message.
* If you write custom loggers or formatters, you will be dealing with objects of this class.
**/
@interface DDLogMessage : NSObject <NSCopying>
{
// Direct accessors to be used only for performance
@public
NSString *_message;
DDLogLevel _level;
DDLogFlag _flag;
NSInteger _context;
NSString *_file;
NSString *_fileName;
NSString *_function;
NSUInteger _line;
id _tag;
DDLogMessageOptions _options;
NSDate *_timestamp;
NSString *_threadID;
NSString *_threadName;
NSString *_queueLabel;
}
/**
* Default `init` is not available
*/
- (instancetype)init NS_UNAVAILABLE;
/**
* Standard init method for a log message object.
* Used by the logging primitives. (And the macros use the logging primitives.)
*
* If you find need to manually create logMessage objects, there is one thing you should be aware of:
*
* If no flags are passed, the method expects the file and function parameters to be string literals.
* That is, it expects the given strings to exist for the duration of the object's lifetime,
* and it expects the given strings to be immutable.
* In other words, it does not copy these strings, it simply points to them.
* This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters,
* so it makes sense to optimize and skip the unnecessary allocations.
* However, if you need them to be copied you may use the options parameter to specify this.
*
* @param message the message
* @param level the log level
* @param flag the log flag
* @param context the context (if any is defined)
* @param file the current file
* @param function the current function
* @param line the current code line
* @param tag potential tag
* @param options a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction.
* @param timestamp the log timestamp
*
* @return a new instance of a log message model object
*/
- (instancetype)initWithMessage:(NSString *)message
level:(DDLogLevel)level
flag:(DDLogFlag)flag
context:(NSInteger)context
file:(NSString *)file
function:(NSString *)function
line:(NSUInteger)line
tag:(id)tag
options:(DDLogMessageOptions)options
timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER;
/**
* Read-only properties
**/
/**
* The log message
*/
@property (readonly, nonatomic) NSString *message;
@property (readonly, nonatomic) DDLogLevel level;
@property (readonly, nonatomic) DDLogFlag flag;
@property (readonly, nonatomic) NSInteger context;
@property (readonly, nonatomic) NSString *file;
@property (readonly, nonatomic) NSString *fileName;
@property (readonly, nonatomic) NSString *function;
@property (readonly, nonatomic) NSUInteger line;
@property (readonly, nonatomic) id tag;
@property (readonly, nonatomic) DDLogMessageOptions options;
@property (readonly, nonatomic) NSDate *timestamp;
@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID
@property (readonly, nonatomic) NSString *threadName;
@property (readonly, nonatomic) NSString *queueLabel;
@end
- 下面这篇参考文章中,在自定义的
log
中显示等级、类和方法、代码行数是个不错的做法。
使用CocoaLumberjack和XcodeColors实现分级Log和控制台颜色
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
NSString *logLevel = nil;
switch (logMessage->_flag) {
case DDLogFlagError:
logLevel = @"[ERROR] > ";
break;
case DDLogFlagWarning:
logLevel = @"[WARN] > ";
break;
case DDLogFlagInfo:
logLevel = @"[INFO] > ";
break;
case DDLogFlagDebug:
logLevel = @"[DEBUG] > ";
break;
default:
logLevel = @"[VBOSE] > ";
break;
}
NSString *formatLog = [NSString stringWithFormat:@"%@[%@ %@][line %ld] %@",
logLevel, logMessage->_fileName, logMessage->_function,
logMessage->_line, logMessage->_message];
return formatLog;
}
自定义的logger
根据网上内容利用 CocoaLumberjack 搭建自己的 Log 系统修改而来
- 从类
DDAbstractDatabaseLogger
继承而来,需要包含头文件#import <CocoaLumberjack/DDAbstractDatabaseLogger.h>
- 这个类的作用是将log保存在数据库中,这个类没有暴露出来。相对来说,数据库比文件系统操作要方便。所以用这个类,而不用文件类
DDFileLogger
- 保持默认设置就好了,达到500条或者间隔1分钟就保存;磁盘数据库保留7天,删除操作间隔5分钟。
- 保存在数据库中内容取得不方便,所以手机数据库中的内容我们不关心,他能正常工作就好了。
- 每一次执行
log
,函数db_log
就会执行。在这里,我们把每条log都保存在一个数组中。比如@property (nonatomic, strong) NSMutableArray *logs;
- 每一次保存
log
,函数db_save
就会执行。在这里,我们把缓存在数组中的log
拼接成一个大字符串(\n
分隔),发送给后台。向后台发送成功后,清空这个缓存数组。 - 至于数据库中的内容,我们不用关心。
- 监听系统消息
UIApplicationWillResignActiveNotification
,在应用回到后台前,保存一下,向服务器发送一次。
#import "XXXLogger.h"
#import "MyAFNetWorking.h"
// 达到500条就发送,所以缓存的数组最大容量达到2000的话,说明网络出了问题
#define kLogCacheCapacity 2000
@interface XXXLogger ()
@property (nonatomic, strong) NSMutableArray *logs;
@end
@implementation XXXLogger
// 生命周期函数
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (instancetype)init {
self = [super init];
if (self) {
self.logs = [NSMutableArray array];
// 使用默认的配置。达到500条或者间隔1分钟就保存;磁盘数据库保留7天,删除操作间隔5分钟,这两个数据不关心,用基类的就可以了
self.saveThreshold = 500; // 达到500条就保存传后台
self.saveInterval = 60; // 60s定时到就保存传后台
// 监听UIApplicationWillResignActiveNotification消息,在程序进入后台前保存log
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
}
return self;
}
// 重写父类函数
- (BOOL)db_log:(DDLogMessage *)logMessage {
// _logFormatter只能用下划线变量访问,不能用self的方式,否则会触发断言
if (!_logFormatter) {
//没有指定 formatter
return NO;
}
if ([self.logs count] > kLogCacheCapacity) {
// 如果段时间内进入大量log,并且迟迟发不到服务器上,我们可以判断哪里出了问题,在这之后的 log 暂时不处理了。
// 但我们依然要告诉 DDLog 这个存进去了。
return YES;
}
//利用 formatter 得到消息字符串,添加到缓存
@synchronized (self) {
// _logFormatter只能用下划线变量访问,不能用self的方式,否则会触发断言
[self.logs addObject:[_logFormatter formatLogMessage:logMessage]];
}
return YES;
}
- (void)db_save {
//如果缓存内没数据,啥也不做
if (0 == [self.logs count]) {
return;
}
// 用换行符,把所有的数据拼成一个大字符串
NSString *logsString = [self.logs componentsJoinedByString:@"\n"];
// 发送给服务器,将AFNetworking包一层作为网络传输
NSString *url = @""; // 根据实际修改
NSDictionary *logs = @{@"log": logsString}; // key值跟后台商量好
__weak __typeof(self) weakSelf = self;
[[MyAFNetWorking shareAfnetworking] performRequestWithPath:url formDataDic:logs success:^(NSDictionary *responseObject) {
// 已经成功传到服务器,之后将缓存清空
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf clearLogs];
} failure:^(NSDictionary *responseObject) {
// 啥也不做
}];
}
// selector
- (void)onWillResignActive:(NSNotification *)notification {
dispatch_async(self.loggerQueue, ^{
[self db_save];
});
}
// 清空缓存
- (void)clearLogs {
@synchronized (self) {
[self.logs removeAllObjects];
}
}
@end
日志颜色
支持XCodeColors插件,根据日志等级显示不同的颜色。不过XCode8之后插件都不能用了,所以就不用折腾了。
下面是一些文章链接,可以看看
robbiehanson/XcodeColors
Xcode8 插件失效不能用
Xcode升级后插件失效解决办法
CocoaLumberjack使用
日志保存到阿里云
- 日志可以发送到自己的后台,怎么发送,和后台商量好就行
- 阿里云提供了日志服务器,提供了日志发送的API,可以将客户端的日志直接发送到阿里云。运维可以通过工具查看,这样就绕过后台,也减轻了后台的压力。
-
lujiajing1126/AliyunLogObjc
这个是阿里提供的log
上传接口,目前的热度还很低 - 支持
Carthage
,不支持CocoaPods
,这个有点特别。 - 如果工程还要支持
iOS7
,还是直接将源码导入工程比较好,不需要库管理工具。如果是Swift
开发的工程,用Carthage
就很方便。
#import <AliyunLogObjc/AliyunLogObjc.h>
LogClient *client = [[LogClient alloc] initWithApp: @"endpoint" accessKeyID:@"" accessKeySecret:@"" projectName:@""];
LogGroup *logGroup = [[LogGroup alloc] initWithTopic: @"" andSource:@""];
Log *log1 = [[Log alloc] init];
[log1 PutContent: @"Value" withKey: @"Key"];
[logGroup PutLog:log1];
[client PostLog:logGroup logStoreName: @"" call:^(NSURLResponse* _Nullable response,NSError* _Nullable error) {
if (error != nil) {
}
}];
- 仅有日志上传功能,并没有日志本地管理功能。可以配合
CocoaLumberjack
使用。 - 如果只是简单将日志发送阿里云,单独使用也是可以的。