887 lines
30 KiB
Objective-C
887 lines
30 KiB
Objective-C
//
|
||
// 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
|
||
|