首页 > 解决方案 > 单元格的父 vc 中的 Swift -AVPlayer' KVO 导致 Xcode 冻结

问题描述

我有一个占据整个屏幕的单元格,因此一次只有一个可见单元格。在单元格内,我有一个 AVPlayer。在单元格的父 vc 内,我有一个 KVO 观察者,它监听"timeControlStatus". 当播放器停止播放时,我stopVideo()会在单元格内调用一个函数来停止播放器并显示重播按钮或播放按钮等。

3个问题:

1-如果我不在 KVO 中使用 DispatchQueue,当视频停止/结束时,当调用单元格的函数时,应用程序会崩溃。

2-当视频停止并且我确实在 KVO 中使用 DispatchQueue 时,它​​的观察者会继续观察并且 Xcode 冻结(没有崩溃)。KVO 中有一个打印语句可以无限打印,这就是 Xcode 冻结的原因。阻止它的唯一方法是杀死 Xcode,否则死亡的沙滩球会继续旋转。

3- 在 KVO 中,我尝试使用通知发送到单元来代替调用cell.stopVideo()函数,但单元内的函数永远不会运行。

我该如何解决这个问题?

应该注意的是,在 KVO 之外无法正常工作,其他一切工作正常。我有一个周期性的时间观察器,每当我滚动时,每个单元格都能完美运行,视频加载正常,当我按下单元格停止/播放视频时,一切正常。

细胞:

protocol MyCellDelegate: class {
    func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?)
}

var player: AVPlayer?
var indexPath: IndexPath?

var playerItem: AVPlayerItem? { 
    didSet {
         // add playerItem to player
         delegate?.sendBackPlayerAndIndexPath(player, indexPath)
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)

    player = AVPlayer()
    // set everything else relating to the player   
}

// both get initialized in cellForItem
var delegate: MyCellDelegate?
var myModel: MyModel? {
     didSet {
          let url = URL(string: myModel!.videUrlStr!)
          asset = AVAsset(url: url)
          playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["playable"])
     }
}

// tried using this with NotificationCenter but it didn't trigger from parent vc
@objc public func playVideo() {
    if !player?.isPlaying {
       player?.play()
    }
    // depending on certain conditions show a mute button, etc
}

// tried using this with NotificationCenter but it didn't trigger from parent vc
@objc public func stopVideo() {
    player?.pause()
    // depending on certain conditions show a reload button or a play button etc
}

父VC

MyVC: ViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

var player: AVPlayer?
var currentIndexPath: IndexPath?
var isObserving = false

func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?) {

    if isObserving {
        self.player?.removeObserver(self, forKeyPath: "status", context: nil)
        self.player?.removeObserver(self, forKeyPath: "timeControlStatus", context: nil)
    }

    guard let p = player, let i = currentIndexPath else { return }

    self.player = p
    self.currentIndexPath = i

    isObserving = true
    self.player?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
    self.player?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}

// If I don't use DispatchQueue below the app crashes
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    if object as AnyObject? === player {
        if keyPath == "status" {
            if player?.status == .readyToPlay {
                    DispatchQueue.main.async { [weak self] in
                        self?.playVideoInCell()
                    }
                }
            }
        } else if keyPath == "timeControlStatus" {

            if player?.timeControlStatus == .playing {
                DispatchQueue.main.async { [weak self] in
                    self?.playVideoInCell()
                }

            } else {

                print("3. Player is Not Playing *** ONCE STOPPED THIS PRINTS FOREVER and Xcode freezes but doesn't crash.\n")
                DispatchQueue.main.async { [weak self] in
                    self?.stopVideoInCell()
                }
            }
        }
    }
}

func playVideoInCell() {
    guard let indexPath = currentIndexPath else { return }
    guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }

    cell.playVideo()
    // also tried sending a NotificationCenter message to the cell but it didn't trigger
}

func stopVideoInCell() {
    guard let indexPath = currentIndexPath else { return }
    guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }

    cell.stopVideo()
    // also tried sending a NotificationCenter message to the cell but it didn't trigger
}

在评论中@matt 要求提供崩溃日志(仅在不使用 KVO 内的 DispatchQueue 时发生)。我启用了僵尸,但它没有给我任何信息。崩溃瞬间发生,然后就变成了空白。它没有给我任何信息。我必须快速截屏才能获得照片,否则它会在崩溃后立即消失。

在此处输入图像描述

在此处输入图像描述

在第 3 个永久打印的 KVO 内部,我删除了 DispatchQueue 并添加了该stopVideoInCell()函数。当函数调用单元格的stopVideo()函数时, player?.pause会短暂地出现EXC_BAD_ACCESS (code=2, address=0x16b01ff0)崩溃。

在此处输入图像描述

然后应用程序终止。在控制台内,唯一打印的内容是:

来自调试器的消息:LLDB RPC 服务器已崩溃。崩溃日志位于 ~/Library/Logs/DiagnosticReports 并具有前缀“lldb-rpc-server”。请提交错误并附上最新的崩溃日志

当我去终端查看打印出来的东西时,我得到的唯一东西就是lldb-rpc-server_2020-06-14-155514_myMacName.crash我遇到这次崩溃的所有日子里的一堆陈述。

使用 DispatchQueue 时不会发生这种情况,并且视频确实会停止,但当然 KVO 中的打印语句会永远运行并且 Xcode 会冻结。

标签: iosswiftuicollectionviewcellavplayerkey-value-observing

解决方案


问题是,在您的属性观察者中,您正在对正在观察的属性进行更改。那是一个恶性循环,一个无限递归;Xcode 通过冻结您的应用程序来显示这一点,直到您最终崩溃(哦,具有讽刺意味)堆栈溢出。

让我们举一个更简单、独立的例子。我们在界面中有一个 UISwitch。开启。如果用户将其关闭,我们希望检测到并将其切换回打开。该示例是一种愚蠢的方法,但它完美地说明了您面临的问题:

class ViewController: UIViewController {
    @IBOutlet weak var theSwitch: UISwitch!
    class SwitchHelper: NSObject {
        @objc dynamic var switchState : Bool = true
    }
    let switchHelper = SwitchHelper()
    var observer: NSKeyValueObservation!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.observer = self.switchHelper.observe(\.switchState, options: .new) { 
            helper, change in
            self.theSwitch.isOn.toggle()
            self.theSwitch.sendActions(for: .valueChanged)
        }
    }
    @IBAction func doSwitch(_ sender: Any) {
        self.switchHelper.switchState = (sender as! UISwitch).isOn
    }
}

会发生什么?用户关闭开关。我们观察到,如switchState; 作为回应,我们将开关切换回 On 并调用sendActions. 和sendActions变化switchState。但是我们仍然处于观察代码的中间switchState!所以我们再做一次,它又发生了。一次又一次,它再次发生。无限循环...

你将如何摆脱困境?你需要以某种方式打破递归。我可以想到两种明显的方法。一种是对自己进行思考,“嗯,我只关心从 On 到 Off 的切换。我不在乎其他方式。” 假设这是真的,您可以使用简单的 解决问题if,就像您选择使用的解决方案一样:

    self.observer = self.switchHelper.observe(\.switchState, options: .new) {
        helper, change in
        if let val = change.newValue, !val {
            self.theSwitch.isOn.toggle()
            self.theSwitch.sendActions(for: .valueChanged)
        }
    }

有时我喜欢使用的更复杂的解决方案是在触发观察者时停止观察,进行任何更改,然后再次开始观察。你必须提前计划一点来实现它,但有时它是值得的:

var observer: NSKeyValueObservation!
func startObserving() {
    self.observer = self.switchHelper.observe(\.switchState, options: .new) {
        helper, change in
        self.observer?.invalidate()
        self.observer = nil
        self.theSwitch.isOn.toggle()
        self.theSwitch.sendActions(for: .valueChanged)
        self.startObserving()
    }
}
override func viewDidLoad() {
    super.viewDidLoad()
    self.startObserving()
}

看起来是递归的,因为startObserving它调用了它自己,但实际上它不是,因为它在调用它时所做的是配置观察;在我们观察到变化之前,内部花括号中的代码不会运行。

(在现实生活中,我可能会将 NSKeyValueObservation 设置为该配置中的局部变量。但这只是一种额外的优雅,对于示例而言并不重要。)


推荐阅读