import Foundation import os.log /** * 文件日志工具 * 将日志同时输出到控制台和文件系统 */ @objc public class FileLogger: NSObject { private static let TAG = "FileLogger" private static var instance: FileLogger? private let logDir: URL private var currentLogFile: URL? private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility) private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter }() private let timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss.SSS" return formatter }() /** * 初始化日志系统 */ private init(directory: URL) { self.logDir = directory.appendingPathComponent("logs", isDirectory: true) super.init() // 创建日志目录 try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) // 创建当天日志文件 updateLogFile() // 记录初始化完成 self.d(FileLogger.TAG, "文件日志系统已初始化") } /** * 更新日志文件为当天日期 */ private func updateLogFile() { let today = dateFormatter.string(from: Date()) let logFile = logDir.appendingPathComponent("log_\(today).txt") if currentLogFile == nil || currentLogFile?.path != logFile.path { currentLogFile = logFile // 如果是新文件,添加文件头 if !FileManager.default.fileExists(atPath: logFile.path) { let headerDate = DateFormatter() headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss" let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n" try? headerLine.write(to: logFile, atomically: true, encoding: .utf8) } } } /** * 将日志写入文件 */ private func logToFile(level: String, tag: String, message: String) { logQueue.async { [weak self] in guard let self = self else { return } do { // 检查是否需要更新日志文件 self.updateLogFile() guard let logFile = self.currentLogFile, FileManager.default.fileExists(atPath: logFile.path) else { return } // 格式化日志内容 let timestamp = self.timeFormatter.string(from: Date()) let logLine = "\(timestamp) \(level)/\(tag): \(message)\n" // 获取文件句柄并追加内容 if let fileHandle = try? FileHandle(forWritingTo: logFile) { fileHandle.seekToEndOfFile() if let data = logLine.data(using: .utf8) { fileHandle.write(data) } fileHandle.closeFile() } else { // 如果无法获取文件句柄,尝试直接写入 let existingContent = try? String(contentsOf: logFile, encoding: .utf8) let newContent = (existingContent ?? "") + logLine try newContent.write(to: logFile, atomically: true, encoding: .utf8) } } catch { print("写入日志文件失败: \(error.localizedDescription)") } } } /** * 调试级别日志 */ private func d(_ tag: String, _ message: String) { // 输出到控制台 print("D/\(tag): \(message)") // 写入文件 logToFile(level: "D", tag: tag, message: message) } /** * 信息级别日志 */ private func i(_ tag: String, _ message: String) { // 输出到控制台 print("I/\(tag): \(message)") // 写入文件 logToFile(level: "I", tag: tag, message: message) } /** * 警告级别日志 */ private func w(_ tag: String, _ message: String) { // 输出到控制台 print("W/\(tag): \(message)") // 写入文件 logToFile(level: "W", tag: tag, message: message) } /** * 错误级别日志 */ private func e(_ tag: String, _ message: String, _ error: Error? = nil) { var fullMessage = message // 如果有错误,添加错误信息 if let error = error { fullMessage += "\n\(error.localizedDescription)" } // 输出到控制台 print("E/\(tag): \(fullMessage)") // 写入文件 logToFile(level: "E", tag: tag, message: fullMessage) } /** * 清理旧日志文件 */ private func cleanOldLogs(daysToKeep: Int = 7) { logQueue.async { do { let currentDate = Date() let fileManager = FileManager.default let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: []) for fileURL in logFiles where fileURL.pathExtension == "txt" { if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), let creationDate = fileAttributes[.creationDate] as? Date { let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate) if let days = age.day, days > daysToKeep { try? fileManager.removeItem(at: fileURL) print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)") } } } } catch { print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)") } } } // MARK: - 公共静态方法 /** * 初始化日志系统 */ @objc public static func initialize() { if instance == nil { let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] instance = FileLogger(directory: documentsDirectory) } } /** * 初始化日志系统(使用自定义目录) */ @objc public static func initialize(withDirectory directory: URL) { if instance == nil { instance = FileLogger(directory: directory) } } /** * 调试日志 */ @objc public static func d(_ tag: String, _ message: String) { // 如果实例不存在,先使用默认目录初始化 if instance == nil { initialize() } instance?.d(tag, message) // 使用系统日志 if #available(iOS 10.0, *) { os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message) } } /** * 信息日志 */ @objc public static func i(_ tag: String, _ message: String) { if instance == nil { initialize() } instance?.i(tag, message) // 使用系统日志 if #available(iOS 10.0, *) { os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message) } } /** * 警告日志 */ @objc public static func w(_ tag: String, _ message: String) { if instance == nil { initialize() } instance?.w(tag, message) // 使用系统日志 if #available(iOS 10.0, *) { os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message) } } /** * 错误日志 */ @objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) { if instance == nil { initialize() } instance?.e(tag, message, error) // 使用系统日志 if #available(iOS 10.0, *) { let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage) } } /** * 清理旧日志 */ @objc public static func cleanOldLogs(daysToKeep: Int = 7) { instance?.cleanOldLogs(daysToKeep: daysToKeep) } /** * 关闭日志系统 */ @objc public static func shutdown() { instance = nil } }