歌曲缓存相关功能

1. 核心组件

MusicCacheManager (音乐缓存管理器)
  • 单例模式:确保全局只有一个实例,方便管理。

    swift 复制代码
    private static var instance: MusicCacheManager?
    
    static func shared() -> MusicCacheManager {
        if instance == nil {
            instance = MusicCacheManager()
        }
        return instance!
    }
  • 文件管理 :使用FileManager管理缓存目录和文件操作。

    swift 复制代码
    private let fileManager = FileManager.default
    private let musicCacheDirectory: URL
  • URLSession:用于下载音乐文件,配置忽略系统代理设置。

    swift 复制代码
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.default
        config.connectionProxyDictionary = [:] // 忽略系统代理设置
        return URLSession(configuration: config)
    }()

2. 主要功能实现

缓存存储
  • 缓存路径:每个音乐文件使用唯一的缓存键(歌曲ID_标题.mp3)进行存储。

    swift 复制代码
    private func getCacheKey(for song: Songs) -> String {
        return "\(song.id)_\(song.title.replacingOccurrences(of: " ", with: "_")).mp3"
    }
  • 检查缓存:判断文件是否已缓存。

    swift 复制代码
    func isCached(song: Songs) -> Bool {
        return getCachedPath(for: song) != nil
    }
缓存下载
  • 下载任务 :使用URLSession下载音乐文件,下载完成后将临时文件移动到缓存目录。

    swift 复制代码
    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) {
            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(最近最少使用)算法清理旧文件。

    swift 复制代码
    private 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. 智能缓存策略

  • 自动缓存控制 :通过MusicPlayerManagerautoCache属性控制是否自动缓存。

    swift 复制代码
    var autoCache: Bool = true
  • 缓存优先级:播放时优先使用缓存文件,无缓存时从网络加载并同时进行缓存。

    swift 复制代码
    if 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配置,忽略系统代理设置提高下载速度,设置合理的超时时间。

    swift 复制代码
    config.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 ...
相关推荐
关注我:程序猿之塞伯坦4 分钟前
JavaScript 性能优化实战:突破瓶颈,打造极致 Web 体验
开发语言·前端·javascript
雨出20 分钟前
算法学习第十七天:LRU缓存与布隆过滤器
学习·算法·缓存
郭涤生1 小时前
第二章:影响优化的计算机行为_《C++性能优化指南》notes
开发语言·c++·笔记·性能优化
pursue_my_life1 小时前
Golang中间件的原理与实现
开发语言·后端·中间件·golang
@小匠1 小时前
使用 Python包管理工具 uv 完成 Open WebUI 的安装
开发语言·python·uv
code bean1 小时前
【C#】关键字 volatile
开发语言·c#
若汝棋茗1 小时前
C# 异步方法设计指南:何时使用 await 还是直接返回 Task?
开发语言·c#
程序员yt1 小时前
双非一本Java方向,学完感觉Java技术含量不高,考研换方向如何选择?
java·开发语言·考研
小宋要上岸1 小时前
基于 Qt / HTTP/JSON 的智能天气预报系统测试报告
开发语言·qt·http·json
MobiCetus1 小时前
如何一键安装所有Python项目的依赖!
开发语言·jvm·c++·人工智能·python·算法·机器学习