封装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!),调用时机异常时会直接触发空指针崩溃 - 直接观察跨层级keyPath
AVPlayer.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
相关产品推荐
相关产品推荐

