【作者前言】:13年入圈,分享些本人工作中遇到的点点滴滴那些事儿,17年刚开始写博客,高手勿喷!以分享交流为主,欢迎各路豪杰点评改进!
1.应用场景:
很多时候,需要我们将项目的日志留存到文件中,或通过手机分享、Xcode调试、上传服务器 等形式,对日志进行分析;
这里也是应公司项目需要,写了一个小工具类,没那么长时间去写-直接一个类处理了(不喜勿喷~~~😄),有需要的盆友可以参考、使用
如有帮助,欢迎点赞/收藏/留言🤝
2.实现目标:
通过自定义宏实现多形式的日志打印,debug环境下同步输出控制台,便于调试;可开启是否写入文件等
上一版在实际使用的过程中 - 发现了一些问题 - 后来进行了演进和修改
- 新增线程安全的兼容,考虑到多线程访问日志类,带来的线程污染日志数据等问题,确保控制台输出与文件I/O完全一致
- 降低对文件I/O的操作频率,提升稳定性和性能
- 新增日志等级控制接口,取消以前的强制写入,采用等级去控制,低于设置的日志等级不再加入文件写入的队列之中
- 新增动态配置日志存储根目录、配置存储空间、配置最大保留时长等接口,新增可自定义设置日志标识符,为检索日志内容提供便利
- 考虑到类的编译加载的性能消耗等问题 - 去除多余类,采用单一类,降低不必要的性能消耗
性能测试中,发现一些问题 - 继续优化,调整 -> 按需使用UI调试回显,避免过多的内存占用,同时进一步优化I/O机制,通过任务降级+定时监测+阈值触发+同步写入的接口,进一步降低I/O频率和内存影响
懒得看???demo直通车[1]
使用效果图示:
Debug模式下,控制台同步:
image.png
对应的日志文件效果:
image.png
3.代码说明:
主要通过自写小工具类YPLogTool实现日志打印、同步文件写入、crash捕捉、日志上传等
1)支持多用户日志存取,数据文件存储结构说明:
image.png
2)支持通过日志等级动态是否需要写入文件流、info/warn/error 多形式打印日志的 便捷宏
image.png
image.png
3)支持设置日志文件的最大存储空间(Mb)
image.png
4)支持设置日志文件的保留天数 (与3相比,具有优先级)
image.png
5)支持日志脱敏的开关控制
image.png
7)支持获取当次打印的所有数据,可用于一些图形化日志数据的回显等
image.png
8)核心代码实现(列举下写文件的吧,多了看了也烦😄~~~具体看本文底部,我会附上demo地址)
YPLogTool.m
//MARK: - yp_printLogWithContentModel: For Xcode
+ (void)yp_printLogWithContentModel:(YPLogContentModel *)contentModel {
yp_printLogTimes ++;
if (contentModel.keyIdentifierStr.length) {
fprintf(contentModel.logLevel == YP_LOG_LEVEL_ERROR ? stderr : stdout,"%s [%s] [%s] %s [%s:%lu] [%s] [%s] :%s\n",contentModel.printLogLevelFlagUTF8, contentModel.timeStrUTF8, contentModel.fmtLogLevelStrUTF8, contentModel.keyIdentifierStrUTF8, contentModel.fileUTF8, (unsigned long)contentModel.line, contentModel.functionNameUTF8, contentModel.threadFlagUTF8, contentModel.formatUTF8);
}else {
fprintf(contentModel.logLevel == YP_LOG_LEVEL_ERROR ? stderr : stdout,"%s [%s] [%s] [%s:%lu] [%s] [%s] :%s\n",contentModel.printLogLevelFlagUTF8, contentModel.timeStrUTF8, contentModel.fmtLogLevelStrUTF8, contentModel.fileUTF8, (unsigned long)contentModel.line, contentModel.functionNameUTF8, contentModel.threadFlagUTF8, contentModel.formatUTF8);
}
if (!(yp_printLogTimes % 5000)) { //
if (contentModel.logLevel == YP_LOG_LEVEL_ERROR) {
fflush(stderr);
}else {
fflush(stdout);
}
}
}
//MARK: - yp_writeLogWithContentModel: For File Save
+ (void)yp_writeLogWithContentModel:(YPLogContentModel *)contentModel multiLinesLog:(NSString *)linesLog{
// 校验下轮转清理
NSString *curDayStr = [[contentModel.timeStr componentsSeparatedByString:@" "][0] stringByReplacingOccurrencesOfString:@"-" withString:@""];
if (yp_useBeginTimeDayStr && (![curDayStr isEqualToString:yp_useBeginTimeDayStr])) {// 当启用日志组件时的日期 与 当前打印日志时的日期 不一致时 && 当前打印日志时的日期有值 , 即跨夜了...
YPLogWarn(@"少年好厉害,决战到天亮!跨夜时间:【%@->%@】", yp_useBeginTimeDayStr, curDayStr);
yp_useBeginTimeDayStr = curDayStr;
if (yp_tempNoClearUserDirectoryNames) {
[yp_tempNoClearUserDirectoryNames removeAllObjects];
}
}
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *filePath = [yp_curUserDirectoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"YPLog_%@_%@.log", yp_curUserIdentifier, curDayStr]];
if(![fileManager fileExistsAtPath:filePath]) {// 如果日志文件不存在 - 创建并写入日志文件
[linesLog writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
YPLogError(@"日志信息写入文件失败,errorInfo: %@ 对应日志内容:%@", error.domain, contentModel.fullLogContent);
}
}else {// 日志文件存在 - 则继续追加写入
NSFileHandle *fileHandle;
@try {
fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
[fileHandle seekToEndOfFile];
NSData* stringData = [linesLog dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle writeData:stringData];
[fileHandle synchronizeFile];
} @catch (NSException *exception) {
} @finally {
// 确保句柄被有效关闭
[fileHandle closeFile];
}
}
// 打印日志写入 次数 ++
yp_writeLogTimes ++;
[[NSUserDefaults standardUserDefaults] setObject:[NSString stringWithFormat:@"%lld", (long long)yp_writeLogTimes] forKey:@"yp_writeLogTimesKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (!(yp_writeLogTimes % 10000)) {// 每10000次打印,校验一回文件大小相关问题,降低频率,增加性能 => 实测 10000次大概是1.*Mb左右
FILE_SIZE_CHECK_LOOP: {
float curFileSize = [YPLoggerTool yp_getTotalLogsSizeMb];
if (curFileSize >= yp_maxFoldSize) {
FILE_EARLIEST_LOOP: {
NSString *earliestFilePath = [YPLoggerTool yp_getEarliestLogFilePath];
if ([earliestFilePath isEqualToString:@"NoFilePath"]) {
return;
}
NSMutableArray *temp = [NSMutableArray arrayWithArray:[earliestFilePath componentsSeparatedByString:@"/"]];
[temp removeLastObject];
NSString *earliestUserPath = [temp componentsJoinedByString:@"/"];
NSInteger userLogsCount = [YPLoggerTool yp_getUserPathLogsCount:earliestUserPath];
if (userLogsCount > yp_maxSaveDays) {
if ([fileManager fileExistsAtPath:earliestFilePath]) {
[fileManager removeItemAtPath:earliestFilePath error:nil];
goto FILE_SIZE_CHECK_LOOP;
}
}else {
NSString *userDirectoryName = temp.lastObject;
[yp_tempNoClearUserDirectoryNames addObject:userDirectoryName];
goto FILE_EARLIEST_LOOP;
}
}
}
}
}
}