如何在UICollectionViewCell中实现带加载器的AVPlayer?Observer未触发怎么办?
UICollectionViewCell中AVPlayer监听失效与视频播放最佳实践
一、解决observeValue未触发的问题
你的代码里有个致命语法错误直接导致KVO监听失效:
添加观察者时,keyPath使用了中文引号:
player?.currentItem!.addObserver(self, forKeyPath: "“timeControlStatus”", options: [.old, .new], context: nil)
把中文的“”替换为英文的"",修正后:
player?.currentItem?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
除此之外,还有几个会引发后续问题的隐患需要修正:
- 全局player变量被复用覆盖:使用全局
player和playerLayer会导致cell复用时旧视频实例被替换,引发播放混乱、监听失效。 - 未移除旧观察者:cell复用后,之前的AVPlayerItem观察者未移除,会造成内存泄漏或监听逻辑混乱,必须在复用或视频销毁时调用
removeObserver(_:forKeyPath:)。 - 重复添加playerLayer:每次复用cell都往playerView的layer中添加新的playerLayer,旧图层未移除会造成图层叠加,浪费性能。
二、UICollectionView中播放视频的最佳实践
1. 自定义UICollectionViewCell封装视频逻辑
将AVPlayer、playerLayer、加载视图封装到cell内部,保证每个cell状态独立,避免控制器逻辑臃肿:
class VideoCollectionViewCell: UICollectionViewCell { var player: AVPlayer? var playerLayer: AVPlayerLayer? let activityIndicator = UIActivityIndicatorView(style: .large) override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } private func setupUI() { // 创建播放容器视图 let playerView = UIView() playerView.tag = 1 playerView.frame = contentView.bounds playerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] contentView.addSubview(playerView) // 配置活动指示器 activityIndicator.center = contentView.center activityIndicator.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] contentView.addSubview(activityIndicator) } func configure(with videoURL: URL) { // 清理旧状态 player?.pause() player?.currentItem?.removeObserver(self, forKeyPath: "timeControlStatus") playerLayer?.removeFromSuperlayer() // 创建新播放实例 player = AVPlayer(url: videoURL) playerLayer = AVPlayerLayer(player: player) playerLayer?.frame = contentView.bounds playerLayer?.videoGravity = .resizeAspectFill if let playerView = contentView.viewWithTag(1) { playerView.layer.addSublayer(playerLayer!) } // 添加状态监听 player?.currentItem?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil) player?.play() } override func prepareForReuse() { super.prepareForReuse() // 复用前重置所有状态 player?.pause() player?.currentItem?.removeObserver(self, forKeyPath: "timeControlStatus") playerLayer?.removeFromSuperlayer() player = nil playerLayer = nil activityIndicator.stopAnimating() } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "timeControlStatus", let change = change else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } let newValue = change[.newKey] as? Int ?? 0 let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue) DispatchQueue.main.async { switch newStatus { case .waitingToPlayAtSpecifiedRate: self.activityIndicator.startAnimating() case .playing, .paused: self.activityIndicator.stopAnimating() default: break } } } }
2. 控制器中简化cell配置
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoCell", for: indexPath) as! VideoCollectionViewCell guard let urlString = videodict[indexPath.item]["vid_url"] as? String, let videoURL = URL(string: urlString) else { return cell } cell.configure(with: videoURL) return cell }
3. 核心优化点
- 按需播放/暂停:监听滚动事件,cell离开可视区域时暂停视频,进入区域时恢复播放,节省资源:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { updateVideoPlaybackState() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { updateVideoPlaybackState() } } private func updateVideoPlaybackState() { let visibleCells = collectionView.visibleCells as! [VideoCollectionViewCell] // 暂停所有可见视频 for cell in visibleCells { cell.player?.pause() } // 仅播放第一个可见视频(可根据需求调整逻辑) if let firstVisibleCell = visibleCells.first { firstVisibleCell.player?.play() } } - 避免强制解包:用
guard或if let替代!强制解包,防止崩溃。 - 内存泄漏防护:所有KVO监听必须在cell复用、实例销毁时移除,使用
weak self避免循环引用。
内容的提问来源于stack exchange,提问作者narasimha
相关产品推荐
相关产品推荐

