自定义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 } }
关键改动说明:
- 移除了致命的子层添加代码:删掉了
self.layer.addSublayer(playerLayer),因为playerLayer本身就是当前视图的根层,不需要再添加一次。 - 补充了资源清理逻辑:新增
cleanUpPreviousMedia方法,在切换媒体类型或视图复用时,清理播放器资源和图片视图,避免内存泄漏和显示混乱。 - 完善视频加载流程:通过
loadValuesAsynchronously异步加载asset属性,确保资源真正可用后再初始化播放器,避免播放失败。 - 优化ImageView复用:保存ImageView引用,避免重复创建子视图,提升性能。
在CollectionView的cellForItemAt方法中,记得在复用Cell时调用mediaPlayerView.prepareForReuse(),确保每次复用都是干净的状态。
内容的提问来源于stack exchange,提问作者BigBoy1337
相关产品推荐
相关产品推荐

