Files
featherVoice/QXLive/Manager/QXLogger.m
2025-12-22 10:16:05 +08:00

887 lines
30 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// QXLogger.m
// QXLive
//
// Created by 启星 on 2025/12/18.
//
#import "QXLogger.h"
#import <sys/utsname.h>
#import <mach/mach.h>
#include <CommonCrypto/CommonCrypto.h>
// 静态变量
static dispatch_queue_t _logQueue = nil;
static QXLogConfig *_config = nil;
static NSMutableArray<NSString *> *_logBuffer = nil;
static NSMutableArray<void (^)(QXLogLevel, NSString *, NSString *)> *_customHandlers = nil;
static BOOL _isEnabled = YES;
static NSDateFormatter *_dateFormatter = nil;
static NSFileHandle *_currentFileHandle = nil;
static NSString *_currentLogFilePath = nil;
static void (^_uploadHandler)(NSArray<NSString *> *, void (^)(BOOL)) = nil;
@implementation QXLogConfig
+ (instancetype)sharedConfig {
static QXLogConfig *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[QXLogConfig alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_minLogLevel = QXLogLevelDebug;
_logTarget = QXLogTargetConsole | QXLogTargetFile;
_maxLogFileSize = 5 * 1024 * 1024; // 5MB
_maxLogFileCount = 10;
_logSaveDays = 7;
_enableNetworkUpload = NO;
_enableConsoleColor = YES;
_addDateToFile = YES;
_enableAsync = YES;
_encryptLogFile = NO;
}
return self;
}
@end
@implementation QXLogger
#pragma mark - 初始化
+ (void)initialize {
if (self == [QXLogger class]) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_logQueue = dispatch_queue_create("com.qx.log.queue", DISPATCH_QUEUE_SERIAL);
_config = [QXLogConfig sharedConfig];
_logBuffer = [NSMutableArray array];
_customHandlers = [NSMutableArray array];
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
// 设置日志系统
[self setupLogger];
});
}
}
+ (void)setupLogger {
// 创建日志目录
[self createLogDirectory];
// 清理过期日志
[self cleanExpiredLogs];
// 查找或创建日志文件
[self findOrCreateLogFile];
// 注册内存警告通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
// 注册崩溃处理
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
[self info:@"QXLogger 初始化完成"];
}
#pragma mark - 核心文件管理方法
+ (void)findOrCreateLogFile {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
// 获取所有日志文件
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (error) {
[self error:@"读取日志目录失败: %@", error.localizedDescription];
[self createNewLogFile];
return;
}
// 过滤出日志文件
NSArray *logFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH 'qxlog_'"]];
if (logFiles.count == 0) {
// 没有日志文件,创建新的
[self info:@"没有找到日志文件,创建新文件"];
[self createNewLogFile];
return;
}
// 找到最新的日志文件(按文件名排序)
NSArray *sortedFiles = [logFiles sortedArrayUsingSelector:@selector(compare:)];
NSString *latestFileName = sortedFiles.lastObject;
NSString *latestFilePath = [logDir stringByAppendingPathComponent:latestFileName];
// 检查文件大小
NSDictionary *attrs = [fm attributesOfItemAtPath:latestFilePath error:nil];
unsigned long long fileSize = attrs ? [attrs fileSize] : 0;
CGFloat fileSizeMB = fileSize / 1024.0 / 1024.0;
CGFloat maxSizeMB = _config.maxLogFileSize / 1024.0 / 1024.0;
if (fileSize < _config.maxLogFileSize) {
// 文件未满,继续使用
[self openExistingLogFile:latestFilePath];
[self info:@"继续使用现有日志文件: %@ (%.2fMB/%.2fMB)", latestFileName, fileSizeMB, maxSizeMB];
} else {
// 文件已满,创建新的
[self createNewLogFile];
[self info:@"日志文件已满,创建新文件"];
}
}
+ (void)openExistingLogFile:(NSString *)filePath {
@synchronized (self) {
// 关闭之前的文件句柄
if (_currentFileHandle) {
@try {
[_currentFileHandle synchronizeFile];
[_currentFileHandle closeFile];
} @catch (NSException *exception) {
[self error:@"关闭文件句柄失败: %@", exception];
}
_currentFileHandle = nil;
}
// 打开现有文件
NSError *error = nil;
_currentFileHandle = [NSFileHandle fileHandleForWritingToURL:[NSURL fileURLWithPath:filePath] error:&error];
if (error) {
[self error:@"打开现有日志文件失败: %@", error.localizedDescription];
[self createNewLogFile];
return;
}
// 移动到文件末尾
@try {
[_currentFileHandle seekToEndOfFile];
} @catch (NSException *exception) {
[self error:@"移动到文件末尾失败: %@", exception];
[self createNewLogFile];
return;
}
_currentLogFilePath = filePath;
// 写入重启标记
[self writeRestartMarker];
}
}
+ (void)createNewLogFile {
@synchronized (self) {
// 关闭之前的文件句柄
if (_currentFileHandle) {
@try {
[_currentFileHandle synchronizeFile];
[_currentFileHandle closeFile];
} @catch (NSException *exception) {
[self error:@"关闭文件句柄失败: %@", exception];
}
_currentFileHandle = nil;
}
// 生成文件名(按日期和序号)
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyyMMdd"];
NSString *dateStr = [formatter stringFromDate:[NSDate date]];
// 获取当天的下一个序号
NSInteger nextIndex = [self getNextFileIndexForDate:dateStr];
NSString *fileName = [NSString stringWithFormat:@"qxlog_%@_%03ld.log", dateStr, (long)nextIndex];
NSString *filePath = [[self logDirectory] stringByAppendingPathComponent:fileName];
// 创建新文件
NSFileManager *fm = [NSFileManager defaultManager];
[fm createFileAtPath:filePath contents:nil attributes:nil];
// 打开文件句柄
NSError *error = nil;
_currentFileHandle = [NSFileHandle fileHandleForWritingToURL:[NSURL fileURLWithPath:filePath] error:&error];
if (error) {
[self error:@"打开日志文件失败: %@", error.localizedDescription];
return;
}
_currentLogFilePath = filePath;
// 写入文件头
[self writeFileHeader];
// 检查文件数量
[self checkLogFileCount];
[self info:@"创建新日志文件: %@", fileName];
}
}
+ (NSInteger)getNextFileIndexForDate:(NSString *)dateStr {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (error) return 1;
NSString *prefix = [NSString stringWithFormat:@"qxlog_%@_", dateStr];
NSArray *todayFiles = [files filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:@"SELF BEGINSWITH %@", prefix]];
if (todayFiles.count == 0) return 1;
// 找出最大序号
NSInteger maxIndex = 0;
for (NSString *fileName in todayFiles) {
// 提取序号qxlog_20240101_001.log -> 001
NSString *baseName = [fileName stringByDeletingPathExtension];
NSArray *components = [baseName componentsSeparatedByString:@"_"];
if (components.count == 3) {
NSString *indexStr = components[2];
NSInteger index = [indexStr integerValue];
if (index > maxIndex) {
maxIndex = index;
}
}
}
return maxIndex + 1;
}
#pragma mark - 文件写入方法
+ (void)writeRestartMarker {
if (!_currentFileHandle) return;
NSMutableString *marker = [NSMutableString string];
[marker appendString:@"\n\n"];
[marker appendString:@"═══════════════════════════════════════════════════\n"];
[marker appendFormat:@"🚀 应用重新启动 - %@\n", [_dateFormatter stringFromDate:[NSDate date]]];
[marker appendString:@"═══════════════════════════════════════════════════\n\n"];
NSData *markerData = [marker dataUsingEncoding:NSUTF8StringEncoding];
@try {
[_currentFileHandle writeData:markerData];
[_currentFileHandle synchronizeFile];
} @catch (NSException *exception) {
[self error:@"写入重启标记失败: %@", exception];
}
}
+ (void)writeFileHeader {
if (!_currentFileHandle) return;
NSMutableString *header = [NSMutableString string];
[header appendString:@"═══════════════════════════════════════════════════\n"];
[header appendString:@"📱 QXLogger 日志文件\n"];
[header appendString:@"═══════════════════════════════════════════════════\n"];
[header appendFormat:@"创建时间: %@\n", [_dateFormatter stringFromDate:[NSDate date]]];
[header appendFormat:@"设备型号: %@\n", [self deviceModel]];
[header appendFormat:@"系统版本: %@ %@\n", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion];
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?:
[[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey];
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"";
NSString *build = [[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey] ?: @"";
[header appendFormat:@"应用信息: %@ %@ (%@)\n", appName ?: @"", version, build];
[header appendFormat:@"文件限制: %.2fMB/文件, 最多%lu个文件\n",
_config.maxLogFileSize / 1024.0 / 1024.0,
(unsigned long)_config.maxLogFileCount];
[header appendString:@"═══════════════════════════════════════════════════\n\n"];
NSData *headerData = [header dataUsingEncoding:NSUTF8StringEncoding];
@try {
[_currentFileHandle writeData:headerData];
[_currentFileHandle synchronizeFile];
} @catch (NSException *exception) {
[self error:@"写入文件头失败: %@", exception];
}
}
#pragma mark - 日志输出
+ (void)outputToFile:(NSString *)log {
@autoreleasepool {
// 检查文件大小
[self checkLogFileSize];
// 确保文件句柄存在
if (!_currentFileHandle) {
[self findOrCreateLogFile];
if (!_currentFileHandle) return;
}
// 写入日志
NSString *logWithNewline = [log stringByAppendingString:@"\n"];
NSData *logData = [logWithNewline dataUsingEncoding:NSUTF8StringEncoding];
@try {
[_currentFileHandle seekToEndOfFile];
[_currentFileHandle writeData:logData];
[_currentFileHandle synchronizeFile];
} @catch (NSException *exception) {
[self error:@"写入日志文件失败: %@", exception];
// 尝试重新创建文件
[self createNewLogFile];
}
}
}
+ (void)checkLogFileSize {
if (!_currentLogFilePath) return;
NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attrs = [fm attributesOfItemAtPath:_currentLogFilePath error:nil];
if (attrs) {
unsigned long long fileSize = [attrs fileSize];
if (fileSize >= _config.maxLogFileSize) {
[self info:@"日志文件达到限制 (%.2fMB),创建新文件", fileSize / 1024.0 / 1024.0];
[self createNewLogFile];
}
}
}
#pragma mark - 基础日志方法实现
+ (void)logWithLevel:(QXLogLevel)level tag:(nullable NSString *)tag format:(NSString *)format args:(va_list)args {
if (!_isEnabled || level < _config.minLogLevel) return;
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
if (_config.enableAsync) {
dispatch_async(_logQueue, ^{
[self processLogWithLevel:level tag:tag message:message];
});
} else {
dispatch_sync(_logQueue, ^{
[self processLogWithLevel:level tag:tag message:message];
});
}
}
+ (void)processLogWithLevel:(QXLogLevel)level tag:(nullable NSString *)tag message:(NSString *)message {
// 格式化日志
NSString *formattedLog = [self formatLogWithLevel:level tag:tag message:message];
// 输出到不同目标
if (_config.logTarget & QXLogTargetConsole) {
[self outputToConsole:formattedLog level:level];
}
if (_config.logTarget & QXLogTargetFile) {
[self outputToFile:formattedLog];
}
// 调用自定义处理器
for (void (^handler)(QXLogLevel, NSString *, NSString *) in _customHandlers) {
handler(level, tag ?: @"", message);
}
}
+ (NSString *)formatLogWithLevel:(QXLogLevel)level tag:(nullable NSString *)tag message:(NSString *)message {
NSString *timestamp = [_dateFormatter stringFromDate:[NSDate date]];
NSString *levelStr = [self stringFromLogLevel:level];
NSString *threadInfo = [NSThread isMainThread] ? @"Main" : @"Background";
NSMutableString *log = [NSMutableString string];
[log appendFormat:@"%@ [%@]", timestamp, threadInfo];
if (_config.logPrefix.length > 0) {
[log appendFormat:@" [%@]", _config.logPrefix];
}
if (tag.length > 0) {
[log appendFormat:@" [%@]", tag];
}
[log appendFormat:@" [%@] %@", levelStr, message];
return [log copy];
}
+ (NSString *)stringFromLogLevel:(QXLogLevel)level {
switch (level) {
case QXLogLevelVerbose: return @"VERBOSE";
case QXLogLevelDebug: return @"DEBUG";
case QXLogLevelInfo: return @"INFO";
case QXLogLevelWarning: return @"WARNING";
case QXLogLevelError: return @"ERROR";
case QXLogLevelFatal: return @"FATAL";
default: return @"UNKNOWN";
}
}
+ (void)outputToConsole:(NSString *)log level:(QXLogLevel)level {
if (_config.enableConsoleColor) {
printf("%s\n", [self coloredLog:log level:level].UTF8String);
} else {
printf("%s\n", log.UTF8String);
}
}
+ (NSString *)coloredLog:(NSString *)log level:(QXLogLevel)level {
NSString *colorCode;
switch (level) {
case QXLogLevelVerbose: colorCode = @"\033[0;37m"; break; // 白色
case QXLogLevelDebug: colorCode = @"\033[0;36m"; break; // 青色
case QXLogLevelInfo: colorCode = @"\033[0;32m"; break; // 绿色
case QXLogLevelWarning: colorCode = @"\033[0;33m"; break; // 黄色
case QXLogLevelError: colorCode = @"\033[0;31m"; break; // 红色
case QXLogLevelFatal: colorCode = @"\033[0;35m"; break; // 紫色
default: colorCode = @"\033[0m";
}
return [NSString stringWithFormat:@"%@%@\033[0m", colorCode, log];
}
#pragma mark - 公开日志方法
+ (void)verbose:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelVerbose tag:nil format:format args:args];
va_end(args);
}
+ (void)verboseWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelVerbose tag:tag format:format args:args];
va_end(args);
}
+ (void)debug:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelDebug tag:nil format:format args:args];
va_end(args);
}
+ (void)debugWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelDebug tag:tag format:format args:args];
va_end(args);
}
+ (void)info:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelInfo tag:nil format:format args:args];
va_end(args);
}
+ (void)infoWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelInfo tag:tag format:format args:args];
va_end(args);
}
+ (void)warning:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelWarning tag:nil format:format args:args];
va_end(args);
}
+ (void)warningWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelWarning tag:tag format:format args:args];
va_end(args);
}
+ (void)error:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelError tag:nil format:format args:args];
va_end(args);
}
+ (void)errorWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelError tag:tag format:format args:args];
va_end(args);
}
+ (void)fatal:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelFatal tag:nil format:format args:args];
va_end(args);
}
+ (void)fatalWithTag:(NSString *)tag format:(NSString *)format, ... {
va_list args;
va_start(args, format);
[self logWithLevel:QXLogLevelFatal tag:tag format:format args:args];
va_end(args);
}
#pragma mark - 特殊日志方法
+ (void)network:(NSString *)method url:(NSString *)url params:(id)params response:(id)response error:(NSError *)error {
NSMutableString *log = [NSMutableString string];
[log appendFormat:@"🌐 %@ %@\n", method, url];
if (params) {
[log appendFormat:@"Params: %@\n", [self jsonStringFromObject:params]];
}
if (response) {
[log appendFormat:@"Response: %@\n", [self jsonStringFromObject:response]];
}
if (error) {
[log appendFormat:@"Error: %@ (Code: %ld)", error.localizedDescription, (long)error.code];
}
[self debugWithTag:@"Network" format:@"%@", log];
}
+ (void)performance:(NSString *)tag startTime:(CFAbsoluteTime)startTime {
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
CFAbsoluteTime duration = (endTime - startTime) * 1000;
NSString *level = duration > 100 ? @"⚠️" : @"";
[self debugWithTag:@"Performance" format:@"%@ %@: %.2fms", level, tag, duration];
}
+ (void)userAction:(NSString *)action params:(NSDictionary *)params {
NSString *paramsStr = params ? [self jsonStringFromObject:params] : @"{}";
[self infoWithTag:@"UserAction" format:@"👤 %@ %@", action, paramsStr];
}
+ (void)crash:(NSException *)exception {
NSString *log = [NSString stringWithFormat:
@"💥 Crash: %@\n"
@"Reason: %@\n"
@"Stack Trace:\n%@",
exception.name,
exception.reason,
exception.callStackSymbols];
[self fatalWithTag:@"Crash" format:@"%@", log];
// 立即刷新到文件
dispatch_sync(_logQueue, ^{
[self outputToFile:log];
});
}
+ (void)memoryWarning {
struct task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
if (kerr == KERN_SUCCESS) {
CGFloat usedMemory = info.resident_size / 1024.0 / 1024.0;
[self warningWithTag:@"Memory" format:@"⚠️ Memory Warning: %.2f MB used", usedMemory];
}
}
#pragma mark - 文件操作方法
+ (NSString *)logDirectory {
NSString *docDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
return [docDir stringByAppendingPathComponent:@"QXLogs"];
}
+ (void)createLogDirectory {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:logDir]) {
NSError *error = nil;
[fm createDirectoryAtPath:logDir withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
[self error:@"创建日志目录失败: %@", error.localizedDescription];
}
}
}
+ (NSArray<NSString *> *)getAllLogFilePaths {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (error) return @[];
// 过滤并排序日志文件
NSArray *logFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH 'qxlog_'"]];
logFiles = [logFiles sortedArrayUsingSelector:@selector(compare:)];
// 转换为完整路径
NSMutableArray *fullPaths = [NSMutableArray array];
for (NSString *file in logFiles) {
[fullPaths addObject:[logDir stringByAppendingPathComponent:file]];
}
return [fullPaths copy];
}
+ (NSString *)getLatestLogFilePath {
NSArray *logFiles = [self getAllLogFilePaths];
return logFiles.lastObject;
}
+ (NSString *)getLogContentFromFile:(NSString *)filePath {
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
return nil;
}
NSError *error = nil;
NSString *content = [NSString stringWithContentsOfFile:filePath
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
[self error:@"读取日志文件失败: %@", error.localizedDescription];
return nil;
}
return content;
}
+ (void)checkLogFileCount {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (!error) {
NSArray *logFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH 'qxlog_'"]];
logFiles = [logFiles sortedArrayUsingSelector:@selector(compare:)];
if (logFiles.count > _config.maxLogFileCount) {
NSUInteger filesToDelete = logFiles.count - _config.maxLogFileCount;
for (NSUInteger i = 0; i < filesToDelete; i++) {
NSString *filePath = [logDir stringByAppendingPathComponent:logFiles[i]];
[fm removeItemAtPath:filePath error:nil];
[self info:@"删除旧日志文件: %@", logFiles[i]];
}
}
}
}
+ (void)cleanExpiredLogs {
if (_config.logSaveDays <= 0) return;
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (error) return;
NSDate *now = [NSDate date];
NSCalendar *calendar = [NSCalendar currentCalendar];
for (NSString *file in files) {
if ([file hasPrefix:@"qxlog_"]) {
NSString *filePath = [logDir stringByAppendingPathComponent:file];
NSDictionary *attrs = [fm attributesOfItemAtPath:filePath error:nil];
if (attrs) {
NSDate *creationDate = [attrs fileCreationDate];
if (creationDate) {
NSDateComponents *components = [calendar components:NSCalendarUnitDay
fromDate:creationDate
toDate:now
options:0];
if (components.day > _config.logSaveDays) {
[fm removeItemAtPath:filePath error:nil];
[self info:@"删除过期日志文件: %@ (创建于: %@)", file, creationDate];
}
}
}
}
}
}
+ (void)cleanAllLogFiles {
NSString *logDir = [self logDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error];
if (!error) {
for (NSString *file in files) {
if ([file hasPrefix:@"qxlog_"]) {
NSString *filePath = [logDir stringByAppendingPathComponent:file];
[fm removeItemAtPath:filePath error:nil];
}
}
}
[self createNewLogFile];
[self info:@"已清除所有日志文件"];
}
#pragma mark - 文件大小管理方法
+ (unsigned long long)getCurrentLogFileSize {
if (!_currentLogFilePath) return 0;
NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attrs = [fm attributesOfItemAtPath:_currentLogFilePath error:nil];
return attrs ? [attrs fileSize] : 0;
}
+ (CGFloat)getCurrentLogFileSizeMB {
return [self getCurrentLogFileSize] / 1024.0 / 1024.0;
}
+ (CGFloat)getTotalLogSizeMB {
NSArray *logFiles = [self getAllLogFilePaths];
NSFileManager *fm = [NSFileManager defaultManager];
CGFloat totalSize = 0;
for (NSString *filePath in logFiles) {
NSDictionary *attrs = [fm attributesOfItemAtPath:filePath error:nil];
if (attrs) {
totalSize += [attrs fileSize] / 1024.0 / 1024.0;
}
}
return totalSize;
}
+ (NSDictionary *)getLogFileInfo:(NSString *)filePath {
NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attrs = [fm attributesOfItemAtPath:filePath error:nil];
if (!attrs) return @{};
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
CGFloat fileSizeMB = [attrs fileSize] / 1024.0 / 1024.0;
NSDate *creationDate = [attrs fileCreationDate];
NSDate *modificationDate = [attrs fileModificationDate];
return @{
@"fileName": [filePath lastPathComponent],
@"fileSize": @([attrs fileSize]),
@"fileSizeMB": @(fileSizeMB),
@"creationDate": creationDate ? [formatter stringFromDate:creationDate] : @"",
@"modificationDate": modificationDate ? [formatter stringFromDate:modificationDate] : @"",
@"filePath": filePath
};
}
+ (NSArray<NSDictionary *> *)getAllLogFileInfos {
NSArray *logFiles = [self getAllLogFilePaths];
NSMutableArray *infos = [NSMutableArray array];
for (NSString *filePath in logFiles) {
[infos addObject:[self getLogFileInfo:filePath]];
}
return [infos copy];
}
+ (void)checkFileSizeManually {
[self checkLogFileSize];
}
#pragma mark - 网络上报
+ (void)uploadLogsWithCompletion:(void (^)(BOOL, NSError * _Nullable))completion {
// 实现上传逻辑
if (completion) completion(NO, nil);
}
+ (void)setUploadHandler:(void (^)(NSArray<NSString *> *, void (^)(BOOL)))handler {
_uploadHandler = [handler copy];
}
#pragma mark - 配置方法
+ (QXLogConfig *)config {
return _config;
}
+ (void)updateConfig:(void (^)(QXLogConfig *))block {
if (block) {
block(_config);
}
}
+ (void)enable:(BOOL)enabled {
_isEnabled = enabled;
}
+ (BOOL)isEnabled {
return _isEnabled;
}
+ (void)addCustomLogHandler:(void (^)(QXLogLevel, NSString *, NSString *))handler {
if (handler) {
[_customHandlers addObject:[handler copy]];
}
}
#pragma mark - 工具方法
+ (NSString *)deviceModel {
struct utsname systemInfo;
uname(&systemInfo);
return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
}
+ (NSString *)jsonStringFromObject:(id)object {
if (!object) return @"null";
if ([object isKindOfClass:[NSString class]]) {
return object;
}
if ([NSJSONSerialization isValidJSONObject:object]) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:object
options:NSJSONWritingPrettyPrinted
error:&error];
if (!error) {
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
}
return [object description];
}
#pragma mark - 崩溃处理
static void uncaughtExceptionHandler(NSException *exception) {
[QXLogger crash:exception];
// 等待日志写入完成
usleep(200000);
// 继续原有的崩溃处理
if (NSGetUncaughtExceptionHandler()) {
NSGetUncaughtExceptionHandler()(exception);
}
}
+ (void)handleMemoryWarning {
[self memoryWarning];
}
@end