单元格的父vc中的Swift -AVPlayer'KVO导致Xcode冻结 [英] Swift -AVPlayer' KVO in cell's parent vc causing Xcode to freeze

查看:64
本文介绍了单元格的父vc中的Swift -AVPlayer'KVO导致Xcode冻结的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个占据整个屏幕的单元格,因此一次只有1个可见单元格.在单元内,我有一个AVPlayer.在单元的父vc内,我有一个监听"timeControlStatus"的KVO观察器.当播放器停止播放时,我在单元格内调用函数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时发生).我启用了僵尸,但没有给我任何信息.崩溃瞬间发生,然后变成空白.它没有给我任何信息.我必须快速拍摄屏幕快照才能获取照片,否则崩溃后它会立即消失.

在KVO中,这是永久打印的第三个打印机,我删除了DispatchQueue,只是添加了stopVideoInCell()函数.当该函数调用单元格的stopVideo()函数时, player?.pause 短暂地发生 EXC_BAD_ACCESS(代码= 2,地址= 0x16b01ff0)崩溃.

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

来自调试器的消息:LLDB RPC服务器已崩溃.崩溃日志 位于〜/Library/Logs/DiagnosticReports中,并带有前缀 'lldb-rpc-server'.请提交错误并附上最新的崩溃信息 日志

当我进入终端并查看打印出的唯一内容时,是我在撞车事故中遇到的所有情况下的一堆lldb-rpc-server_2020-06-14-155514_myMacName.crash语句.

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

解决方案

问题是,在属性观察器中,您正在更改要观察的属性.那是一个恶性循环,一个无限的递归. 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调用了它自己,但实际上不是,因为它被调用时要做的是 configure .在我们观察到变化之前,内部花括号中的代码不会运行.

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

I have a cell that takes up the entire screen so there is only 1 visible cell at a time. Inside the cell I have an AVPlayer. Inside the cell's parent vc I have a KVO observer that listens to the "timeControlStatus". When the player stops playing I call a function stopVideo() inside the cell to stop the player and either show a replay button or a play button etc.

3 problems:

1- When the video stops/reaches end if I don't use DispatchQueue inside the KVO the app crashes when the cell's function is called.

2- When the video stops and I do use DispatchQueue inside the KVO it's observer keeps observing and Xcode freezes (no crash). There is a print statement inside the KVO that prints indefinitely and that's why Xcode freezes. The only way to stop it is to kill Xcode otherwise the beachball of death keeps spinning.

3- In the KVO I tried to use a notification to send to the cell in place of calling the cell.stopVideo() function but the function inside the cell never runs.

How can i fix this issue?

It should be noted that outside of the KVO not working everything else works fine. I have a periodic time observer that runs perfectly for every cell whenever I scroll, the videos load fine, and when I press the cell stop/play the video it all works fine.

cell:

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
}

parent 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
}

In the comments @matt asked for the crash log (only occurs when not using DispatchQueue inside the KVO). I have zombies enabled and it didn't give me any info. The crash happens instantaneously and then just goes blank. It doesn't give me any info. I had to quickly take a screen shot just to get the pic otherwise it disappears right after the crash.

Inside the KVO, the 3rd one that prints forever, I removed the DispatchQueue and just added the stopVideoInCell() function. When the function calls the cell's stopVideo() function, player?.pause briefly gets a EXC_BAD_ACCESS (code=2, address=0x16b01ff0) crash.

The app then terminates. Inside the console the only thing that is printed is:

Message from debugger: The LLDB RPC server has crashed. The crash log is located in ~/Library/Logs/DiagnosticReports and has a prefix 'lldb-rpc-server'. Please file a bug and attach the most recent crash log

When I go to terminal and see what prints out the only thing I get is a bunch of lldb-rpc-server_2020-06-14-155514_myMacName.crash statements from all the days I ran into this crash.

When using DispatchQueue this doesn't occur and the video does stop but of course that print statement inside the KVO runs forever and Xcode freezes.

解决方案

The problem is that, in your property observer, you are making a change in the property that you are observing. That is a vicious circle, an infinite recursion; Xcode displays this by freezing your app until ultimately you crash with (oh, the irony) a stack overflow.

Let's take a simpler, self-contained example. We have a UISwitch in the interface. It's On. If the user switches it Off, we want to detect that and switch it back to On. The example is a silly way to do this, but it perfectly illustrates the issue you are facing:

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
    }
}

What's going to happen? The user turns the switch Off. We are observing that, as switchState; in response, we switch the switch back to On and call sendActions. And sendActions changes switchState. But we are still in the middle of the code that observes switchState! So we do it again and it happens again. And again and it happens again. Infinite loop...

How would you get out of this? You need to break the recursion somehow. I can think of two obvious ways. One is to think to yourself, "Well, I only care about a switch from On to Off. I don't care about the other way." Assuming that's true, you can solve the problem with a simple if, rather like the solution you elected to use:

    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)
        }
    }

A more elaborate solution, which I like to use sometimes, is to stop observing when the observer is triggered, make whatever the change is, and then start observing again. You have to plan ahead a little to implement that, but it's sometimes worth it:

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()
}

That looks recursive, because startObserving calls itself, but it isn't really, because what it does when it is called is to configure the observation; the code in the inner curly braces doesn't run until we get an observed change.

(In real life, I would probably make the NSKeyValueObservation a local variable in that configuration. But that's just an additional bit of elegance, not essential to the point of the example.)

这篇关于单元格的父vc中的Swift -AVPlayer'KVO导致Xcode冻结的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆