1. 核心组件
MusicCacheManager (音乐缓存管理器)
-
单例模式:确保全局只有一个实例,方便管理。
swiftprivate static var instance: MusicCacheManager? static func shared() -> MusicCacheManager { if instance == nil { instance = MusicCacheManager() } return instance! }
-
文件管理 :使用
FileManager
管理缓存目录和文件操作。swiftprivate let fileManager = FileManager.default private let musicCacheDirectory: URL
-
URLSession:用于下载音乐文件,配置忽略系统代理设置。
swiftprivate lazy var session: URLSession = { let config = URLSessionConfiguration.default config.connectionProxyDictionary = [:] // 忽略系统代理设置 return URLSession(configuration: config) }()
2. 主要功能实现
缓存存储
-
缓存路径:每个音乐文件使用唯一的缓存键(歌曲ID_标题.mp3)进行存储。
swiftprivate func getCacheKey(for song: Songs) -> String { return "\(song.id)_\(song.title.replacingOccurrences(of: " ", with: "_")).mp3" }
-
检查缓存:判断文件是否已缓存。
swiftfunc isCached(song: Songs) -> Bool { return getCachedPath(for: song) != nil }
缓存下载
-
下载任务 :使用
URLSession
下载音乐文件,下载完成后将临时文件移动到缓存目录。swiftfunc cacheSong(_ song: Songs, withUrl url: String, completion: @escaping (String?, Error?) -> Void) { guard let downloadUrl = URL(string: url) else { completion(nil, NSError(domain: "MusicCacheManager", code: 1001, userInfo: [ NSLocalizedDescriptionKey: "无效的歌曲URL" ])) return } let cacheKey = getCacheKey(for: song) let destinationPath = musicCacheDirectory.appendingPathComponent(cacheKey).path // 检查是否已经缓存 if fileManager.fileExists(atPath: destinationPath) { completion(destinationPath, nil) return } // 创建下载任务 let downloadTask = session.downloadTask(with: downloadUrl) { [weak self] (tempURL, response, error) in guard let self = self else { return } if let error = error { completion(nil, error) return } guard let tempURL = tempURL else { completion(nil, NSError(domain: "MusicCacheManager", code: 1003, userInfo: [ NSLocalizedDescriptionKey: "下载临时文件URL为空" ])) return } do { // 将临时文件移动到目标位置 try self.fileManager.moveItem(at: tempURL, to: URL(fileURLWithPath: destinationPath)) completion(destinationPath, nil) } catch { completion(nil, error) } } // 开始下载 downloadTask.resume() }
缓存管理
-
清理缓存:当缓存超过最大限制时自动触发清理,基于LRU(最近最少使用)算法清理旧文件。
swiftprivate func cleanupCache() { do { let cacheInfo = cacheConfig.loadCacheInfo() let cachedFiles = try fileManager.contentsOfDirectory(at: musicCacheDirectory, includingPropertiesForKeys: nil) // 将文件按最后访问时间排序 let sortedFiles = cachedFiles.sorted { (url1, url2) -> Bool in let key1 = url1.lastPathComponent let key2 = url2.lastPathComponent let time1 = cacheInfo[key1] ?? 0 let time2 = cacheInfo[key2] ?? 0 return time1 < time2 } // 如果缓存超出限制,删除最早访问的文件 var newSize = currentSize var newCacheInfo = cacheInfo for fileURL in sortedFiles { if newSize <= cacheConfig.maxCacheSize * UInt64(0.8) { break } let key = fileURL.lastPathComponent do { let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) if let fileSize = attributes[.size] as? UInt64 { try fileManager.removeItem(at: fileURL) newSize -= fileSize newCacheInfo.removeValue(forKey: key) } } catch { print("清理缓存文件失败: \(error.localizedDescription)") } } // 保存更新后的缓存信息 cacheConfig.saveCacheInfo(newCacheInfo) } catch { print("执行缓存清理失败: \(error.localizedDescription)") } }
3. 智能缓存策略
-
自动缓存控制 :通过
MusicPlayerManager
的autoCache
属性控制是否自动缓存。swiftvar autoCache: Bool = true
-
缓存优先级:播放时优先使用缓存文件,无缓存时从网络加载并同时进行缓存。
swiftif let cachedPath = cacheManager.getCachedPath(for: data) { playLocalMusic(uri: cachedPath, data: data) } else { playNetworkMusic(uri: uri, data: data) if autoCache { cacheManager.cacheSong(data) { (cachedPath, error) in // 处理缓存结果 } } }
4. 性能优化
-
异步操作 :所有文件操作在后台线程执行,使用
DispatchQueue
确保线程安全。 -
网络优化 :自定义
URLSession
配置,忽略系统代理设置提高下载速度,设置合理的超时时间。swiftconfig.timeoutIntervalForResource = 60.0 // 资源超时时间为60秒 config.timeoutIntervalForRequest = 30.0 // 请求超时时间为30秒
通过这样的设计,音乐缓存系统能够有效提升音乐播放的流畅度,减少网络请求,优化用户体验。

关于断点重传机制:
swift
// ... existing code ...
/// 保存下载进度数据
private var resumeDataDict: [String: Data] = [:]
/// 缓存歌曲,使用指定的URL
///
/// - Parameters:
/// - song: 歌曲对象
/// - url: 下载地址
/// - completion: 完成回调,参数为缓存后的本地路径和可能的错误
func cacheSong(_ song: Songs, withUrl url: String, completion: @escaping (String?, Error?) -> Void) {
guard let downloadUrl = URL(string: url) else {
completion(nil, NSError(domain: "MusicCacheManager", code: 1001, userInfo: [
NSLocalizedDescriptionKey: "无效的歌曲URL"
]))
return
}
let cacheKey = getCacheKey(for: song)
let destinationPath = musicCacheDirectory.appendingPathComponent(cacheKey).path
// 检查是否已经缓存
if fileManager.fileExists(atPath: destinationPath) {
print("歌曲已缓存: \(song.title)")
completion(destinationPath, nil)
return
}
// 检查是否正在下载
if let _ = downloadTasks[cacheKey] {
print("歌曲正在下载中: \(song.title)")
completion(nil, NSError(domain: "MusicCacheManager", code: 1002, userInfo: [
NSLocalizedDescriptionKey: "歌曲正在下载中"
]))
return
}
print("开始下载歌曲: \(song.title), URL: \(url)")
// 创建下载任务
let downloadTask: URLSessionDownloadTask
// 检查是否有可恢复的数据
if let resumeData = resumeDataDict[cacheKey] {
print("使用断点续传数据继续下载: \(song.title)")
downloadTask = session.downloadTask(withResumeData: resumeData)
// 清除已使用的恢复数据
resumeDataDict.removeValue(forKey: cacheKey)
} else {
downloadTask = session.downloadTask(with: downloadUrl)
}
// 设置完成回调
downloadTask.taskDescription = cacheKey
// 将任务添加到队列
downloadTasks[cacheKey] = downloadTask
// 设置完成处理器
downloadTask.resume()
}
/// 取消下载
///
/// - Parameter song: 歌曲对象
func cancelDownload(for song: Songs) {
let cacheKey = getCacheKey(for: song)
if let task = downloadTasks[cacheKey] {
// 使用byProducingResumeData方法取消,以获取恢复数据
task.cancel(byProducingResumeData: { [weak self] data in
guard let self = self else { return }
if let data = data {
// 保存恢复数据以便后续使用
self.resumeDataDict[cacheKey] = data
print("已保存断点续传数据: \(song.title)")
}
self.downloadTasks.removeValue(forKey: cacheKey)
print("已取消下载: \(song.title)")
})
}
}
// 需要添加URLSessionDownloadDelegate方法来处理下载完成事件
extension MusicCacheManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let cacheKey = downloadTask.taskDescription else { return }
// 从下载任务队列中移除
downloadTasks.removeValue(forKey: cacheKey)
// 获取目标路径
let destinationPath = musicCacheDirectory.appendingPathComponent(cacheKey).path
do {
// 如果目标文件已存在,先删除
if fileManager.fileExists(atPath: destinationPath) {
try fileManager.removeItem(atPath: destinationPath)
}
// 将临时文件移动到目标位置
try fileManager.moveItem(at: location, to: URL(fileURLWithPath: destinationPath))
print("歌曲缓存成功: \(cacheKey), 路径: \(destinationPath)")
// 更新最后访问时间
updateAccessTime(for: cacheKey)
// 查找对应的回调并执行
// 这里需要维护一个回调字典,或者通过其他方式找到对应的回调
// 简化起见,这里省略了回调处理
} catch {
print("保存缓存文件失败: \(error.localizedDescription)")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let downloadTask = task as? URLSessionDownloadTask,
let cacheKey = downloadTask.taskDescription else { return }
// 从下载任务队列中移除
downloadTasks.removeValue(forKey: cacheKey)
if let error = error as NSError? {
// 检查是否是取消错误,并且有恢复数据
if error.code == NSURLErrorCancelled &&
error.userInfo[NSURLSessionDownloadTaskResumeData] != nil {
// 已经在cancelDownload方法中处理了恢复数据,这里不需要额外处理
} else {
print("下载任务失败: \(cacheKey), 错误: \(error.localizedDescription)")
}
}
}
}
// 初始化方法中需要修改session的创建方式
private init() {
// ... existing code ...
// 创建URL会话,使用self作为代理
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
// ... existing code ...
}
// ... existing code ...