美文网首页
iOS日志/缓存组件基础库需重点关注IO操作的性能损耗和数据可靠

iOS日志/缓存组件基础库需重点关注IO操作的性能损耗和数据可靠

作者: 利尔德 | 来源:发表于2025-10-02 16:04 被阅读0次

核心是在“高效读写”与“不阻塞主线程”“避免数据丢失”之间找平衡。

需重点注意的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()保证数据可靠性。

相关文章

网友评论

      本文标题:iOS日志/缓存组件基础库需重点关注IO操作的性能损耗和数据可靠

      本文链接:https://www.haomeiwen.com/subject/ntnhtstx.html