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

离开视频页面导致AVPlayer触发NSInternalInconsistencyException崩溃

AVQueuePlayer页面退出崩溃问题修复

崩溃报错信息

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <NSKeyValueObservance 0x2809113b0> for the key path "currentItem.videoComposition from <AVQueuePlayer 0x28079dfc0> most likely because the value for the key "currentItem" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the AVQueuePlayer class

问题根源

崩溃并非因为手动添加的rateObserver未清理,而是:

  • AVPlayerLooper会强持有AVQueuePlayer,导致旧Player无法及时释放
  • AVPlayerViewController内部会对currentItem.videoComposition自动添加KVO观察者,当Player未被正确清理时,这些内部观察者会在对象生命周期外被尝试移除,触发异常
  • updateUIViewController中每次都会创建新Player,旧Player的资源未被主动清理,残留的KVO引发冲突

修复方案

1. 优化Player创建逻辑,避免重复实例化

在makeAVPlayer方法中,先判断当前Player是否符合需求(循环状态、URL匹配),仅在必要时创建新Player,减少不必要的资源重建:

private func makeAVPlayer(in controller: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
    // 检查当前Player是否满足需求,无需重建则直接复用
    let needNewPlayer: Bool
    if isLooping {
        needNewPlayer = !(controller.player is AVQueuePlayer && 
                         controller.player?.currentItem?.asset is AVURLAsset &&
                         (controller.player?.currentItem?.asset as? AVURLAsset)?.url == url)
    } else {
        needNewPlayer = !(controller.player is AVPlayer && 
                         controller.player?.currentItem?.asset is AVURLAsset &&
                         (controller.player?.currentItem?.asset as? AVURLAsset)?.url == url)
    }
    
    guard needNewPlayer else {
        controller.showsPlaybackControls = showsPlaybackControls
        return
    }
    
    // 先清理旧资源
    context.coordinator.loopingPlayer?.stop()
    context.coordinator.loopingPlayer = nil
    controller.player?.pause()
    controller.player = nil
    
    // 创建新Player
    if isLooping {
        let item = AVPlayerItem(url: url)
        let player = AVQueuePlayer(playerItem: item)
        let loopingPlayer = AVPlayerLooper(player: player, templateItem: item)
        controller.videoGravity = AVLayerVideoGravity.resizeAspectFill
        context.coordinator.loopingPlayer = loopingPlayer
        controller.player = player
    } else {
        controller.player = AVPlayer(url: url)
    }

    controller.showsPlaybackControls = showsPlaybackControls
    controller.onPlayerStatusChange = onTimeControlStatusChange
}

2. 完善Player的清理逻辑

在LegacyAVPlayerViewController中,确保Player被替换或销毁时,彻底清理相关资源:

public class LegacyAVPlayerViewController: AVPlayerViewController {
    // ... 原有代码 ...
    
    public override var player: AVPlayer? {
        willSet {
            rateObserver?.invalidate()
            // 暂停旧Player并清空当前Item,避免内部KVO残留
            oldValue?.pause()
            oldValue?.replaceCurrentItem(with: nil)
        }
        didSet {
            rateObserver = player?.observe(\AVPlayer.rate, options: [.new], changeHandler: rateHandler(_:change:))
        }
    }

    deinit {
        rateObserver?.invalidate()
        player?.pause()
        player = nil // 强制置空Player,触发AVPlayerViewController内部资源清理
    }
    
    // ... 原有代码 ...
}

3. 清理AVPlayerLooper资源

在Coordinator的deinit中主动停止并置空Looper,切断对Player的强引用:

public class CustomPlayerCoordinator<Overlay: View>: NSObject, AVPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate {
    // ... 原有代码 ...
    
    deinit {
        loopingPlayer?.stop()
        loopingPlayer = nil
    }
    
    // ... 原有代码 ...
}

修复原理

通过主动清理旧Player和Looper的强引用关系,确保AVPlayerViewController内部的KVO观察者能在对象释放前正确移除,避免因资源残留导致的KVO异常。同时减少不必要的Player实例化,提升性能的同时避免生命周期冲突。

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 03:02:02