离开视频页面导致AVPlayer触发NSInternalInconsistencyException崩溃
崩溃报错信息
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

