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

自定义MediaPlayerView在CollectionView Cell中崩溃:AVPlayerLayer循环问题

解决AVPlayerLayer循环引用导致的CALayerInvalid崩溃问题

你完全找对了问题根源!崩溃的直接原因就是你在setupVideo里做了一个致命操作:把playerLayer(也就是当前视图的根层,因为你重写了layerClass为AVPlayerLayer)添加成了自身的子层,这就形成了层的循环引用,系统检测到后直接抛出CALayerInvalid异常终止应用。

下面是修复后的完整代码,同时我也补充了CollectionView Cell复用场景下的资源清理逻辑,避免后续出现内存泄漏或显示混乱:

import UIKit
import Kingfisher
import AVKit
import AVFoundation

class MediaPlayerView: UIView {
    var mediaURL: URL? {
        didSet {
            determineMediaType()
        }
    }
    
    let videoExtensions = ["mov", "mp4", "m4v"] // 补充常见视频格式
    var asset: AVURLAsset?
    var player: AVPlayer? {
        get { return (layer as? AVPlayerLayer)?.player }
        set { (layer as? AVPlayerLayer)?.player = newValue }
    }
    
    // 保存图片视图引用,方便复用清理
    private var imageView: UIImageView?
    
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
    
    static let assetKeysRequiredToPlay = [
        "playable",
        "hasProtectedContent"
    ]
    
    private var playerItem: AVPlayerItem? = nil
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupInitialState()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupInitialState()
    }
    
    // 初始化通用配置
    private func setupInitialState() {
        // 设置AVPlayerLayer的填充模式
        (layer as? AVPlayerLayer)?.videoGravity = .resizeAspectFill
    }
    
    func determineMediaType() {
        // 先清理之前的资源,适配CollectionView复用
        cleanUpPreviousMedia()
        
        guard let url = mediaURL else { return }
        let pathExtension = url.pathExtension.lowercased()
        
        if videoExtensions.contains(pathExtension) {
            print("Movie URL: \(url)")
            setupVideo(url: url)
        } else {
            print("Image URL: \(url)")
            setupImage(url: url)
        }
    }
    
    func setupVideo(url: URL) {
        asset = AVURLAsset(url: url, options: nil)
        
        // 异步加载asset的必要属性,确保资源可用
        asset?.loadValuesAsynchronously(forKeys: Self.assetKeysRequiredToPlay) { [weak self] in
            guard let self = self else { return }
            DispatchQueue.main.async {
                self.checkAssetStatus()
            }
        }
    }
    
    private func checkAssetStatus() {
        guard let asset = asset else { return }
        
        for key in Self.assetKeysRequiredToPlay {
            var error: NSError?
            if asset.statusOfValue(forKey: key, error: &error) != .loaded {
                print("Asset加载失败: \(error?.localizedDescription ?? "未知错误")")
                return
            }
        }
        
        // 确认资源可播放后初始化player
        if asset.isPlayable && !asset.hasProtectedContent {
            let playerItem = AVPlayerItem(asset: asset)
            self.playerItem = playerItem
            self.player = AVPlayer(playerItem: playerItem)
            // 这里可以根据需求设置自动播放或者暂停
            // self.player?.play()
        }
    }
    
    func setupImage(url: URL) {
        if let existingImageView = imageView {
            // 复用已有的ImageView
            existingImageView.kf.setImage(with: url)
        } else {
            let imageView = UIImageView()
            imageView.frame = bounds
            imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            addSubview(imageView)
            imageView.kf.setImage(with: url)
            self.imageView = imageView
        }
    }
    
    // 清理资源,适配复用场景
    private func cleanUpPreviousMedia() {
        // 暂停并重置播放器
        player?.pause()
        player = nil
        playerItem = nil
        asset = nil
        
        // 移除图片视图(如果存在)
        imageView?.removeFromSuperview()
        imageView = nil
    }
    
    // 视图即将复用的时候可以主动调用这个方法清理
    func prepareForReuse() {
        cleanUpPreviousMedia()
        mediaURL = nil
    }
}

关键改动说明:

  1. 移除了致命的子层添加代码:删掉了self.layer.addSublayer(playerLayer),因为playerLayer本身就是当前视图的根层,不需要再添加一次。
  2. 补充了资源清理逻辑:新增cleanUpPreviousMedia方法,在切换媒体类型或视图复用时,清理播放器资源和图片视图,避免内存泄漏和显示混乱。
  3. 完善视频加载流程:通过loadValuesAsynchronously异步加载asset属性,确保资源真正可用后再初始化播放器,避免播放失败。
  4. 优化ImageView复用:保存ImageView引用,避免重复创建子视图,提升性能。

在CollectionView的cellForItemAt方法中,记得在复用Cell时调用mediaPlayerView.prepareForReuse(),确保每次复用都是干净的状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 03:52:41