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

封装AVPlayer单例时KVO抛出未处理消息异常问题排查

问题根因判断

你的Player单例没有被提前释放。从崩溃日志可以看到,KVO回调触发时Player实例(内存地址0x600001f84cc0)是正常存在的,崩溃的直接原因是:你通过addObserver注册了KVO观察,但没有实现KVO强制要求的回调方法observeValue(forKeyPath:of:change:context:),系统将属性变化事件发送给实例时找不到对应处理方法,直接抛出NSInternalInconsistencyException异常。

除了这个直接崩溃点,你的现有代码还存在多个会引发后续问题的隐患:

  • 每次调用play(urlString:)都会新建AVPlayer实例、重复注册KVO和通知,但从未在旧实例上移除观察,会导致内存泄漏、重复回调甚至野指针崩溃
  • 你使用let center = NotificationCenter()创建了自定义通知中心实例,而非系统全局的NotificationCenter.default,注册的观察者永远收不到系统派发的播放器事件通知
  • 通知Selector拼写错误:你实现的方法名为failedToPlayToEndTime,但注册时传入的Selector是failedToPlayTillEnd,二者不匹配,即便用对通知中心也会因找不到方法抛异常
  • 大量使用隐式解包可选值(presentingVC!、player!),调用时机异常时会直接触发空指针崩溃
  • 直接观察跨层级keyPathAVPlayer.currentItem.status,切换播放资源时极易触发KVO非法状态异常
AVPlayer独立封装最佳实践

封装核心原则:

  • 单例只负责播放器内核逻辑、观察者的注册/生命周期管理,不直接持有UIViewController实例,避免循环引用
  • 所有KVO、通知观察必须在播放器销毁/资源重置时配对移除
  • 用闭包或者代理协议向外抛出播放状态、错误事件,不要在播放器单例内直接实现UI弹窗、页面跳转逻辑
  • 优先使用iOS 11+支持的block版KVO API,避免手写Selector带来的拼写错误、上下文匹配问题

修正后的Player单例实现:

import Foundation
import UIKit
import AVKit

class Player: NSObject {
    static let shared = Player()
    // 播放器核心实例
    private var player: AVPlayer?
    private var playerController: AVPlayerViewController?
    // 观察者持有对象,用于后续移除
    private var statusObservation: NSKeyValueObservation?
    private var currentItemStatusObservation: NSKeyValueObservation?
    // 状态回调,外部VC可监听处理UI
    var onPlayError: ((Error) -> Void)?
    
    private override init() {
        super.init()
        // 初始化时统一注册通知,用系统默认通知中心
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(newErrorLogEntry(_:)),
                                               name: .AVPlayerItemNewErrorLogEntry,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(failedToPlayToEndTime(_:)),
                                               name: .AVPlayerItemFailedToPlayToEndTime,
                                               object: nil)
    }
    
    func play(urlString: String, from presentingVC: UIViewController) {
        // 先重置之前的播放资源、移除旧观察
        resetPlayback()
        
        // 初始化请求头、资源
        let auth = Authentication()
        let headers = ["x-auth-token": auth.token]
        guard let url = URL(string: urlString) else { return }
        let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
        let playerItem = AVPlayerItem(asset: asset)
        
        let player = AVPlayer(playerItem: playerItem)
        self.player = player
        
        // 用block版KVO注册观察,无需手动实现通用回调,自动管理生命周期
        statusObservation = player.observe(\.status, options: [.new, .initial]) { [weak self] player, change in
            guard let self = self else { return }
            // 在这里处理player状态变化,比如状态为.failed时取错误信息
            if player.status == .failed {
                self.onPlayError?(player.error ?? NSError(domain: "PlayerError", code: -1))
            }
        }
        
        currentItemStatusObservation = player.observe(\.currentItem?.status, options: [.new, .initial]) { [weak self] player, change in
            guard let self = self, let item = player.currentItem else { return }
            if item.status == .failed {
                self.onPlayError?(item.error ?? NSError(domain: "PlayerError", code: -1))
            }
        }
        
        // 初始化播放器控制器、弹出页面
        let controller = AVPlayerViewController()
        controller.player = player
        self.playerController = controller
        
        presentingVC.present(controller, animated: true) { [weak self] in
            self?.player?.play()
        }
    }
    
    private func resetPlayback() {
        // 移除旧KVO观察
        statusObservation?.invalidate()
        currentItemStatusObservation?.invalidate()
        statusObservation = nil
        currentItemStatusObservation = nil
        // 暂停、清空旧播放器
        player?.pause()
        player = nil
        //  dismiss旧的播放器页面
        playerController?.dismiss(animated: false)
        playerController = nil
    }
    
    @objc private func newErrorLogEntry(_ notification: Notification) {
        guard let playerItem = notification.object as? AVPlayerItem,
              let errorLog = playerItem.errorLog() else {
            return
        }
        NSLog("Player error log: \(errorLog)")
    }
    
    @objc private func failedToPlayToEndTime(_ notification: Notification) {
        guard let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error else {
            return
        }
        onPlayError?(error)
    }
    
    deinit {
        // 移除通知
        NotificationCenter.default.removeObserver(self)
        resetPlayback()
    }
}

ViewController中的调用方式:

// 调用播放
Player.shared.onPlayError = { [weak self] error in
    guard let self = self else { return }
    // 在VC层处理弹窗UI逻辑
    let alert = UIAlertController(title: "播放失败", message: error.localizedDescription, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: { _ in
        self.dismiss(animated: true)
    }))
    self.present(alert, animated: true)
}
Player.shared.play(urlString: yourPlayURL, from: self)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 07:48:26