将KVO封装在Continuation中是否安全?担忧重复调用致其悬挂
将KVO封装在CheckedThrowingContinuation内部是否安全?
你的顾虑完全正确,当前的实现并不安全,确实会导致悬挂的Continuation,进而引发内存泄漏或不可预期的异常行为。
问题根源
- 你的代码中
observer是类级的可选变量,每次调用play方法时,都会覆盖这个变量,销毁前一次调用创建的NSKeyValueObservation实例。 - 如果前一次
play的KVO回调还没触发就再次调用play,前一次的监听会被直接取消,对应的Continuation永远不会被resume,一直处于悬挂状态。 - 额外隐患:如果
playerItem初始化失败为nil,observe方法返回nil,同样会导致Continuation永久悬挂。
修复方案
方案1:让Observer与Continuation绑定(兼容iOS 13+)
通过将Observer作为闭包局部变量持有,确保在Continuation完成前不会被释放,同时增加取消回调和即时状态检查:
var playerItem: AVPlayerItem? func play(asset: AVURLAsset) async throws { let currentPlayerItem = AVPlayerItem(asset: asset) playerItem = currentPlayerItem let readyToPlay = try await withCheckedThrowingContinuation { continuation in // 创建监听,闭包内持有observer,避免提前释放 let observer = currentPlayerItem.observe(\.status) { item, _ in switch item.status { case .readyToPlay: continuation.resume(returning: true) case .failed: let error = item.error ?? NSError( domain: "PlayerError", code: -1, userInfo: [NSLocalizedDescriptionKey: "播放资源加载失败"] ) continuation.resume(throwing: error) case .unknown: // 资源仍在加载,继续等待回调 return @unknown default: let error = NSError( domain: "PlayerError", code: -2, userInfo: [NSLocalizedDescriptionKey: "不支持的播放状态"] ) continuation.resume(throwing: error) } } // Task取消时主动销毁监听 continuation.onCancel = { observer.invalidate() } // 即时检查状态,避免不必要的等待 switch currentPlayerItem.status { case .readyToPlay: continuation.resume(returning: true) observer.invalidate() case .failed: let error = currentPlayerItem.error ?? NSError( domain: "PlayerError", code: -1, userInfo: [NSLocalizedDescriptionKey: "播放资源加载失败"] ) continuation.resume(throwing: error) observer.invalidate() default: break } } // 后续播放逻辑 ... }
方案2:用AsyncSequence封装KVO(iOS 15+)
iOS 15及以上版本可以直接使用Foundation提供的KeyValueObservingSequence,自动管理监听生命周期,无需手动处理Continuation:
var playerItem: AVPlayerItem? func play(asset: AVURLAsset) async throws { let currentPlayerItem = AVPlayerItem(asset: asset) playerItem = currentPlayerItem // 将KVO转为AsyncSequence,自动管理监听 for await status in currentPlayerItem.values(for: \.status) { switch status { case .readyToPlay: return case .failed: throw currentPlayerItem.error ?? NSError( domain: "PlayerError", code: -1, userInfo: [NSLocalizedDescriptionKey: "播放资源加载失败"] ) case .unknown: continue @unknown default: throw NSError( domain: "PlayerError", code: -2, userInfo: [NSLocalizedDescriptionKey: "不支持的播放状态"] ) } } }
额外注意事项
- 避免用全局
playerItem覆盖前一次的实例,如果需要中断前一次播放,应主动取消对应的Task。 - 处理
AVPlayerItem.error时要考虑nil情况,提供默认错误信息。 unknown状态通常是资源加载中,建议继续等待而非直接抛出错误。
内容的提问来源于stack exchange,提问作者Jack Guo
相关产品推荐
相关产品推荐

