iOS OC版轻量日志的实现
iOS项目记录日志的需求
- 可以在代码上控制日志的级别,以便打印不同层级的日志
- 需要同时在控制台、文件写入
- 需要和NSLog一样的调用格式,方便替换
- 需要保证顺序写入,不能发生由于并发写入错误的问题
- 以后可以扩展输入到别的地方,比如浏览器、app内其他UI上显示日志等
- 日志文件大小限定、设定清理N天前的日志
解决思路
-
设计LogLevel,能够实现 类似LogCat的效果。预计只设计NONE、ERROR、INFO、DEBUG。分别表示
- NONE 什么都不打印 - ERROR 只打印ERROR - INFO 打印INFO和ERROR - DEBUG 全部打印
-
利用iOS的本地广播通知的功能,在打印日志的接口里,将最终的字符串从广播发出去。在不同的文件日志类、控制台类、其他UI界面注册此通知,分别处理字符串。这同时解决了扩展性的问题。
-
实现OC语言风格的可变参数 以及 C语言风格的日志打印接口
-
本地广播,在接收的对象处理的时候天然就是顺序的了,不会有多线程的竞争问题
-
每次写完文件日志,检查日志文件大小,超过重新创建新文件。每次启动对象检查是否有过期文件,有则删除
知识点
- 本地广播的发送API
- 可变参数方法:写文件 vfprintf、控制台 vprintf
- NSString 转 char*
- GCD串行队列
- OC类实现类似NSLog风格的API,并且也能够被C语言调用
- 日志存储路径 Document 以及查询日志文件的修改日期、大小等API
实现代码:
参考了 Log4OC ,改造成我的版本。
注意,可以监听本地广播,将日志内容做额外处理或者显示到UI等
Log4OC.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Log4OC : NSObject
#define MYLOG_NotificationName @"com.MyLog.Notification"
//日志级别
typedef NS_ENUM(NSUInteger,LogLevel) {
LogLevel_ERROR = 0b00001111, //全部打印
LogLevel_DEBUG = 0b00000111, //打印WARN、INFO、DEBUG
LogLevel_WARN = 0b00000011, //打印INFO和WARN
LogLevel_INFO = 0b00000001, //只打印INFO的日志
LogLevel_NONE = 0b00000000 //不打印
};
//获取当前的日志文件路径
FOUNDATION_EXPORT NSString* getLogDir(void);
//设置日志级别
FOUNDATION_EXPORT void setLogLevel(LogLevel level);
//设置]保留多少天的日志
FOUNDATION_EXPORT void setRollBackDay(int days);
FOUNDATION_EXPORT void HXLog_ERROR(NSString *message,...) NS_FORMAT_FUNCTION(1,2);
FOUNDATION_EXPORT void HXLog_DEBUG(NSString *message,...) NS_FORMAT_FUNCTION(1,2);
FOUNDATION_EXPORT void HXLog_WARN(NSString *message,...) NS_FORMAT_FUNCTION(1,2);
FOUNDATION_EXPORT void HXLog_INFO(NSString *message,...) NS_FORMAT_FUNCTION(1,2);
@end
Log4OC.m
//
// Log4OC.m
#import "Log4OC.h"
@implementation Log4OC
static LogLevel __LogLevel = LogLevel_DEBUG;
static int __fileDays = 15;//15day
static dispatch_queue_t __serialQueue;
static NSDateFormatter *__LogFormatter;
static NSString * __filePath = @"";
static NSString * __dirc = @"";
static FILE *__logOutp;
void setLogLevel(LogLevel level){
__LogLevel = level;
}
void setRollBackDay(int days){
__fileDays = days;
}
+(void)load {
__LogFormatter = [[NSDateFormatter alloc] init];
__LogFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.sss";
//创建日志目录,只做一次
creatLogDir();
//删除失效的文件
deleteFiles(__dirc);
//创建日志文件
creatLogFile(__dirc);
NSLog(@"LOG日志路径:%@",__filePath);
//打开输出文件
__logOutp = fopen([__filePath UTF8String],"a+");
setvbuf(__logOutp, NULL, _IONBF, 0);
__serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);//创建串行队列
}
inline NSString* getLogDir(void){
return __dirc;
}
static inline void creatLogDir (){
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dirc = [documentsDirectory stringByAppendingPathComponent:@"LogLog"];
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:dirc]) {
[fm createDirectoryAtPath:dirc withIntermediateDirectories:YES attributes:nil error:nil];
}
__dirc = dirc;
}
static inline void creatLogFile (NSString* path){
NSFileManager *fm = [NSFileManager defaultManager];
NSDateFormatter *fmt = [[NSDateFormatter alloc]init];
fmt.dateFormat = @"yyyy_MM_dd";
NSDate *nowDay = [NSDate new];
NSString *timeStr = [fmt stringFromDate:nowDay];
NSString *filePath = [path stringByAppendingString:[NSString stringWithFormat:@"/%@.log",timeStr]];
if (![fm fileExistsAtPath:filePath]) {
[fm createFileAtPath:filePath contents:nil attributes:nil];
}
__filePath = filePath;
}
static inline void openNextDateFile () {
//删除失效的文件
deleteFiles(__dirc);
//创建日志文件
creatLogFile(__dirc);
NSLog(@"LOG日志路径:%@",__filePath);
if (__logOutp) {
fclose(__logOutp);
}
//打开输出文件
__logOutp = fopen([__filePath UTF8String],"a+");
setvbuf(__logOutp, NULL, _IONBF, 0);
}
static inline int compareOneDay(NSDate *oneDay,NSDate *anotherDay)
{
NSString *oneDayStr = [__LogFormatter stringFromDate:oneDay];
NSString *anotherDayStr = [__LogFormatter stringFromDate:anotherDay];
NSDate *dateA = [__LogFormatter dateFromString:oneDayStr];
NSDate *dateB = [__LogFormatter dateFromString:anotherDayStr];
NSComparisonResult result = [dateA compare:dateB];
// NSLog(@"oneDay : %@, anotherDay : %@", oneDay, anotherDay);
if (result == NSOrderedDescending) {
//在指定时间前面 过了指定时间 过期
// NSLog(@"oneDay is in the future");
return 1;
}
else if (result == NSOrderedAscending){
//没过指定时间 没过期
// NSLog(@"Date1 is in the past");
return -1;
}
//刚好时间一样.
//NSLog(@"Both dates are the same");
return 0;
}
static void deleteFiles(NSString* path){
NSFileManager *fm = [NSFileManager defaultManager];
NSDate *nowDay = [NSDate new];
NSTimeInterval day = 24 * 60 * 60 * __fileDays;
NSDate *day10before = [nowDay dateByAddingTimeInterval:-day];
//遍历文件夹
NSError *error = nil;
NSArray *dirArray = [fm contentsOfDirectoryAtPath:path error:&error];
if (dirArray) {
NSLog(@"日志目录下的文件:");
NSString *oneFilePath = nil;
for(int i = 0; i < dirArray.count;i++){
oneFilePath = [dirArray objectAtIndex:i];
NSString *realFilePath = [path stringByAppendingString:[NSString stringWithFormat:@"/%@",oneFilePath]];
NSDictionary *fileAttributes = [fm attributesOfItemAtPath:realFilePath error:&error];
if (fileAttributes) {
NSNumber *fileSize;
NSString *creationDate;
NSDate *fileModDate;
//文件大小
if ((fileSize = [fileAttributes objectForKey:NSFileSize])) {
NSLog(@" %@:(size=%qi)\n",oneFilePath ,[fileSize unsignedLongLongValue]);
}
//文件修改日期
if ((fileModDate = [fileAttributes objectForKey:NSFileModificationDate])) {
//NSLog(@"Modification date: %@\n", fileModDate);
if (compareOneDay(day10before,fileModDate)>0) {
BOOL blDele= [fm removeItemAtPath:realFilePath error:nil];
if (!blDele) {
NSLog(@"删除已过期日志文件失败");
}
}
}
//文件创建日期
else if ((creationDate = [fileAttributes objectForKey:NSFileCreationDate])) {
//NSLog(@"File creationDate: %@\n", creationDate);
if (compareOneDay(day10before,fileModDate)>0) {
BOOL blDele= [fm removeItemAtPath:realFilePath error:nil];
if (!blDele) {
NSLog(@"删除已过期日志文件失败");
}
}
}
}else{
NSLog(@"ERROR : %@\n", error);
}
}
}
}
void privateLog(NSString *string){
dispatch_async(__serialQueue,^{
//发送本地通知
[[NSNotificationCenter defaultCenter] postNotificationName:HXLOG_NotificationName object:string];
NSDate *nowDay = [NSDate new];
NSDateFormatter *fmt = [[NSDateFormatter alloc]init];
fmt.dateFormat = @"yyyy_MM_dd";
NSString *oneDayStr = [fmt stringFromDate:nowDay];
if (![__filePath containsString:oneDayStr]) {
openNextDateFile();
}
const char *cString = [string UTF8String];
//写文件
fprintf(__logOutp,"%s",cString);
//写控制台
printf("%s",cString);
});
}
void HXLog_ERROR(NSString *message,...){
if ((__LogLevel & LogLevel_ERROR) != LogLevel_ERROR) {
return;
}
va_list argList;
va_start(argList, message);
NSString *string = [NSString stringWithFormat:@"%@ [ERROR] : %@\n",[__LogFormatter stringFromDate:[NSDate date]],[[NSString alloc] initWithFormat:message arguments:argList]];
va_end(argList);
privateLog(string);
}
void HXLog_DEBUG(NSString *message,...){
if ((__LogLevel & LogLevel_DEBUG) != LogLevel_DEBUG) {
return;
}
va_list argList;
va_start(argList, message);
NSString *string = [NSString stringWithFormat:@"%@ [DEBUG] : %@\n",[__LogFormatter stringFromDate:[NSDate date]],[[NSString alloc] initWithFormat:message arguments:argList]];
va_end(argList);
privateLog(string);
}
void HXLog_WARN(NSString *message,...){
if ((__LogLevel & LogLevel_WARN) != LogLevel_WARN) {
return;
}
va_list argList;
va_start(argList, message);
NSString *string = [NSString stringWithFormat:@"%@ [ WARN] : %@\n",[__LogFormatter stringFromDate:[NSDate date]],[[NSString alloc] initWithFormat:message arguments:argList]];
va_end(argList);
privateLog(string);
}
void HXLog_INFO(NSString *message,...){
if ((__LogLevel & LogLevel_INFO) != LogLevel_INFO) {
return;
}
va_list argList;
va_start(argList, message);
NSString *string = [NSString stringWithFormat:@"%@ [ INFO] : %@\n",[__LogFormatter stringFromDate:[NSDate date]],[[NSString alloc] initWithFormat:message arguments:argList]];
va_end(argList);
privateLog(string);
}
@end