核心是在“高效读写”与“不阻塞主线程”“避免数据丢失”之间找平衡。
需重点注意的4个IO操作问题
1. 避免主线程IO:日志写入、缓存读写等IO操作是耗时任务,若在主线程执行会导致UI卡顿(如滑动列表时写日志导致掉帧)。
解决方案:通过GCD后台队列(如dispatch_queue_create("IO.Queue", DISPATCH_QUEUE_SERIAL))统一处理所有IO操作。
2. 控制IO频率(合并写入):高频日志(如每秒多条)若每条都触发一次文件写入,会频繁操作磁盘,严重消耗性能。
解决方案:采用“批量合并”策略,比如累计10条日志或间隔10秒,再一次性写入文件;缓存更新也可先放内存暂存,达到阈值后批量同步到磁盘。
3. 保证数据可靠性(防丢失):突然断电、App崩溃时,未写入磁盘的日志/缓存会丢失,尤其缓存组件需避免关键数据缺失。
解决方案:
日志:关键日志(如支付、崩溃日志)可强制“同步写入”(用FileHandle的synchronizeFile()方法,确保数据刷到磁盘)。
缓存:采用“内存+磁盘”双缓存,内存修改后标记“待同步”,后台定时同步,同时在App启动时先加载磁盘缓存到内存。
4. 限制IO资源占用:无限制存储日志/缓存会导致磁盘空间耗尽,触发系统清理或App被闪退。
解决方案:
日志:设置文件大小上限(如单文件50MB)和文件数量上限(如保留最近10个文件),超过后自动删除 oldest 文件。
缓存:设置总缓存容量上限(如100MB),当达到阈值时,按“LRU(最近最少使用)”策略清理不常用缓存。
以下是日志组件IO优化的核心代码模板(Swift),整合了后台队列、批量写入、日志轮转三大核心优化点,可直接适配基础日志组件开发:
1. 核心工具类定义(含后台队列与批量配置)
import Foundation
class LogManager {
// 单例避免重复创建队列
static let shared = LogManager()
// 后台串行队列(串行保证写入顺序,避免文件竞争)
private let ioQueue = DispatchQueue(label: "com.log.io.queue", qos:.utility)
// 批量写入配置
private let batchThreshold = 10 // 累计10条日志触发写入
private let batchInterval = 10.0 // 间隔10秒强制写入(避免长时间缓存)
// 内存暂存日志的数组
private var logBuffer: [String] = []
// 定时器(用于间隔写入)
private var writeTimer: Timer?
private init() {
// 初始化时启动定时器
setupTimer()
}
// 启动定时写入定时器
private func setupTimer() {
writeTimer = Timer.scheduledTimer(withTimeInterval: batchInterval, repeats: true) { [weak self] _ in
self?.flushLogs() // 到时间触发写入
}
// 确保定时器在后台队列执行
RunLoop.current.add(writeTimer!, forMode:.common)
}
}
2. 日志入队(内存暂存,不直接写磁盘)
extension LogManager {
/// 外部调用:添加日志(仅存内存,不阻塞主线程)
/// - Parameter content: 日志内容
func addLog(_ content: String) {
// 切换到后台队列操作缓冲区(避免线程安全问题)
ioQueue.async { [weak self] in
guard let self = self else { return }
// 拼接日志时间戳
let time = DateFormatter.localizedString(from: Date(), dateStyle:.medium, timeStyle:.medium)
let log = "[\(time)] \(content)"
self.logBuffer.append(log)
// 达到批量阈值,立即触发写入
if self.logBuffer.count >= self.batchThreshold {
self.flushLogs()
}
}
}
}
3. 批量写入与日志轮转(核心IO操作)
extension LogManager {
/// 批量写入磁盘(仅在后台队列执行)
private func flushLogs() {
guard!logBuffer.isEmpty else { return }
// 1. 取出缓冲区日志,清空缓冲区(避免重复写入)
let logsToWrite = logBuffer.joined(separator: "\n") + "\n"
logBuffer.removeAll()
// 2. 获取日志文件路径(Documents/Logs目录下)
guard let logDir = getLogDirectory(),
let currentLogFile = getCurrentLogFile(in: logDir) else {
print("日志目录/文件创建失败")
return
}
// 3. 写入文件(追加模式)
do {
let fileHandle = try FileHandle(forWritingTo: currentLogFile)
fileHandle.seekToEndOfFile() // 跳到文件末尾,避免覆盖
fileHandle.write(logsToWrite.data(using:.utf8)!)
fileHandle.synchronizeFile() // 关键:强制刷盘,保证数据不丢失
fileHandle.closeFile()
// 4. 检查是否需要日志轮转(单文件50MB,保留10个文件)
checkLogRotation(in: logDir, maxFileSize: 50 * 1024 * 1024, maxFileCount: 10)
} catch {
print("日志写入失败:\(error)")
}
}
/// 获取/创建日志目录(Documents/Logs)
private func getLogDirectory() -> URL? {
let docsDir = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!
let logDir = docsDir.appendingPathComponent("Logs")
if!FileManager.default.fileExists(atPath: logDir.path) {
try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
}
return logDir
}
/// 获取当前日志文件(按日期命名,如20240520.log)
private func getCurrentLogFile(in dir: URL) -> URL? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
let fileName = "\(dateFormatter.string(from: Date())).log"
return dir.appendingPathComponent(fileName)
}
/// 日志轮转(删除旧文件)
private func checkLogRotation(in dir: URL, maxFileSize: Int64, maxFileCount: Int) {
do {
// 1. 检查当前文件大小,超过上限则重命名(加后缀.1/.2)
let currentFile = getCurrentLogFile(in: dir)!
let fileSize = try FileManager.default.attributesOfItem(atPath: currentFile.path)[.size] as! Int64
if fileSize > maxFileSize {
let newName = currentFile.deletingPathExtension().appendingPathExtension("1").appendingPathExtension("log")
try FileManager.default.moveItem(at: currentFile, to: newName)
}
// 2. 按修改时间排序,保留最新的maxFileCount个文件
let allLogs = try FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: [.contentModificationDateKey])
.sorted { a, b in
let dateA = try! a.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate!
let dateB = try! b.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate!
return dateA > dateB // 倒序:最新的在前
}
// 3. 删除超出数量的旧文件
if allLogs.count > maxFileCount {
let oldLogs = allLogs[maxFileCount...]
for oldLog in oldLogs {
try FileManager.default.removeItem(at: oldLog)
}
}
} catch {
print("日志轮转失败:\(error)")
}
}
}
4. 外部调用示例(主线程调用无阻塞)
// 任意线程(如主线程)调用,仅入内存缓冲区
LogManager.shared.addLog("用户点击登录按钮")
LogManager.shared.addLog("网络请求成功:statusCode=200")
该模板已覆盖核心优化:所有IO操作在后台队列执行,通过内存缓冲区批量写入降低IO频率,同时通过logRotation控制磁盘占用,synchronizeFile()保证数据可靠性。










网友评论