You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 11:13:13