// // MP_CacheManager.swift // MusicPlayer // // Created by Mr.Zhou on 2024/5/23. // import Foundation ///缓存实体资源 class CachedMedia: NSObject, Codable { //数据 let data: Data //数据块 let dataBlocks:[MediaDataBlock] //是否完整 let isComplete: Bool //最大长度 let maxCount:Int64 init(data: Data, dataBlocks:[MediaDataBlock], isComplete: Bool, maxCount:Int64) { self.data = data self.dataBlocks = dataBlocks self.isComplete = isComplete self.maxCount = maxCount } } ///缓存管理工具 class MP_CacheManager { // 单例模式,提供全局访问点 static let shared = MP_CacheManager() // 缓存实体,字典形式,键值1为videoID,键值2为对应的资源数据 private let memoryCache = NSCache() //文件管理器 private let fileManager = FileManager.default //缓存文件地址 private let cacheDirectory: URL //缓存专用线程 private var cacheQueue = DispatchQueue(label: "com.MP_CacheManager.cacheQueue") //缓存专用 private var cacheOperations: [String: DispatchWorkItem] = [:] //专用节流时间间隔 private let throttleInterval: TimeInterval = 1.0 //固定缓存时间 private let expirationInterval: TimeInterval = 86400 // 24小时 private init() { //获取全部文件地址 let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask) //设置缓存文件地址 cacheDirectory = urls[0].appendingPathComponent("MyAssetCacheManager") //检查缓存文件地址是否存在 if !fileManager.fileExists(atPath: cacheDirectory.path) { //不存在就创建一个 try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) } } /// 保存缓存数据 /// - Parameters: /// - data: 数据本身 /// - key: 使用videoId作为对应的键值 func cacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) { // 在你的 cacheQueue 里执行操作来确保线程安全 cacheQueue.async { [weak self] in // 取消这个 key 的任何现有缓存操作 self?.cacheOperations[key]?.cancel() // 创建一个新的缓存操作 let operation = DispatchWorkItem { self?.performCacheData(data, dataBlocks: dataBlocks, forKey: key, isComplete: isComplete, maxCount: maxCount) // 甚至删除值也包含在cacheQueue的异步调用中 self?.cacheQueue.async { self?.cacheOperations.removeValue(forKey: key) } } // 把 operation 保存进字典 self?.cacheOperations[key] = operation // 安排 operation 在节流间隔后运行 self?.cacheQueue.asyncAfter(deadline: .now() + self!.throttleInterval, execute: operation) } } //保存缓存数据 private func performCacheData(_ data: Data, dataBlocks:[MediaDataBlock], forKey key: String, isComplete: Bool = false, maxCount:Int64) { let contentToCache = CachedMedia(data: data, dataBlocks: dataBlocks, isComplete: isComplete, maxCount: maxCount) memoryCache.setObject(contentToCache, forKey: key as NSString) //文件路径 let fileURL = self.cacheDirectory.appendingPathComponent(key) //临时文件 let tempURL = fileURL.appendingPathExtension("tmp") let encoder = JSONEncoder() do { let newData = try encoder.encode(contentToCache) // 写入到文件 try newData.write(to: tempURL, options: .atomicWrite) // 检查目标位置是否已经存在文件,如果存在,则删除 if self.fileManager.fileExists(atPath: fileURL.path) { try self.fileManager.removeItem(at: fileURL) } // 然后将临时文件移动到最终位置 try self.fileManager.moveItem(at: tempURL, to: fileURL) // 设置过期时间元数据 let expirationDate = Date().addingTimeInterval(self.expirationInterval) try self.fileManager.setAttributes([.modificationDate: expirationDate], ofItemAtPath: fileURL.path) } catch { // 如果发生错误,删除临时文件 try? self.fileManager.removeItem(at: tempURL) print("无法将密钥数据写入磁盘 \(key) - 错误: \(error)") } } /// 取出数据 /// - Parameter key: 使用videoId作为对应的键值 /// - Returns: 返回的数据 func data(forKey key: String) -> CachedMedia? { if let cachedContent = memoryCache.object(forKey: key as NSString){ return cachedContent } return cacheQueue.sync { [weak self] in guard let self = self else { return nil } let fileURL = self.cacheDirectory.appendingPathComponent(key) guard let jsonData = try? Data(contentsOf: fileURL) else { return nil } // 将从磁盘中读取到的 JSON 数据反序列化为 CachedMedia 对象 let decoder = JSONDecoder() do{ let cachedContent = try decoder.decode(CachedMedia.self, from: jsonData) // 检查数据是否过期(同时可以在此处加入完整性检查) let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) guard let modificationDate = attributes[.modificationDate] as? Date else { return nil } if Date().timeIntervalSince(modificationDate) < expirationInterval{ // 数据未过期,返回数据并更新内存缓存 memoryCache.setObject(cachedContent, forKey: key as NSString) return cachedContent } else { // 数据已过期,删除缓存文件 try fileManager.removeItem(at: fileURL) } } catch { print("读取缓存数据失败 \(key) - 错误: \(error)") } return nil } } }