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

如何在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)

除此之外,还有几个会引发后续问题的隐患需要修正:

  1. 全局player变量被复用覆盖:使用全局player和playerLayer会导致cell复用时旧视频实例被替换,引发播放混乱、监听失效。
  2. 未移除旧观察者:cell复用后,之前的AVPlayerItem观察者未移除,会造成内存泄漏或监听逻辑混乱,必须在复用或视频销毁时调用removeObserver(_:forKeyPath:)。
  3. 重复添加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 20:06:19