如何用Swift Concurrency异步加载AVQueuePlayer避免主线程阻塞?
解决大量音频资源异步加载到AVQueuePlayer的问题
针对你的场景(iOS 16+,200个短音频资源,需无阻塞加载并保持播放顺序),以下是正确的实现方案:
核心问题分析
- 同步加载的问题:直接通过
urls.map(AVAsset.init).map(AVPlayerItem.init)创建资源时,AVAsset会同步尝试读取媒体元数据,大量资源会阻塞主线程。 - 你之前异步代码的错误:
- TaskGroup的遍历逻辑错误,不应在添加任务的循环内遍历结果
- 未维护原始URL的顺序,并行加载完成的资源顺序随机,无法支持段落跳转
- 错误使用可选链式调用
player?.insert(player为非可选实例)
正确实现代码
let audioURLs: [URL] = // 你的音频URL数组 let queuePlayer = AVQueuePlayer() // 在后台异步执行加载逻辑,不阻塞主线程 Task { do { // 创建任务组,并行加载每个音频资源,同时记录原始索引以保证顺序 let sortedLoadedItems = try await withThrowingTaskGroup(of: (index: Int, item: AVPlayerItem).self) { group in // 为每个URL启动加载任务 for (index, url) in audioURLs.enumerated() { group.addTask { let asset = AVAsset(url: url) // 异步加载必要资源属性(触发媒体数据预加载,避免播放时阻塞) _ = try await asset.load(.duration) let playerItem = AVPlayerItem(asset: asset) // 可选:加载播放状态,确保资源就绪 _ = try await playerItem.load(.playbackStatus) return (index, playerItem) } } // 收集所有加载完成的结果 var loadedResults = [(index: Int, item: AVPlayerItem)]() for try await result in group { loadedResults.append(result) } // 按原始索引排序,保证播放顺序与URL列表一致 return loadedResults.sorted(by: { $0.index < $1.index }).map(\.item) } // 回到主线程更新AVQueuePlayer(AVFoundation操作需在主线程执行) await MainActor.run { sortedLoadedItems.forEach { item in queuePlayer.insert(item, after: nil) } } // 可选:启动播放 queuePlayer.play() } catch { // 处理加载错误(如个别资源加载失败) print("资源加载失败:\(error.localizedDescription)") } }
关键细节说明
- 并行加载与顺序保证:通过TaskGroup并行加载每个资源,同时记录原始索引,加载完成后按索引排序,确保队列顺序与原始URL列表一致,满足跳转指定段落的需求。
- 异步资源预加载:调用
asset.load(.duration)和playerItem.load(.playbackStatus)触发异步加载,避免后续播放时的主线程阻塞。 - 主线程安全:所有对AVQueuePlayer的修改操作通过
MainActor.run回到主线程执行,符合AVFoundation的线程安全要求。
可选优化
- 若需支持加载进度展示:可在TaskGroup内部维护已完成任务数,通过
@MainActor标记的属性更新UI进度。 - 若需跳过失败资源:可将任务组的返回类型改为
(index: Int, item: AVPlayerItem?),在任务内部捕获错误并返回nil,最后过滤掉nil项。
内容的提问来源于stack exchange,提问作者TruMan1
相关产品推荐
相关产品推荐

