// // QXLogger.m // QXLive // // Created by 启星 on 2025/12/18. // #import "QXLogger.h" #import #import #include // 静态变量 static dispatch_queue_t _logQueue = nil; static QXLogConfig *_config = nil; static NSMutableArray *_logBuffer = nil; static NSMutableArray *_customHandlers = nil; static BOOL _isEnabled = YES; static NSDateFormatter *_dateFormatter = nil; static NSFileHandle *_currentFileHandle = nil; static NSString *_currentLogFilePath = nil; static void (^_uploadHandler)(NSArray *, 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 *)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 *)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 *, 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