Swift -AVPlayer' KVO in cell's parent vc causing Xcode to freeze

耗尽温柔 提交于 2021-01-28 03:55:42

问题


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.


回答1:


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




回答2:


I resolved this using a Boolean. It's not the most elegant answer but it works. If someone can come up with a better answer I'll accept it. It doesn't make sense what's going because of what I put under more:

answer:

var isPlayerStopped = false

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 {

                if isPlayerStopped { return }

                print("3. Player is Not Playing *** NOW THIS ONLY PRINTS ONCE.\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 }

    isPlayerStopped = false

    cell.playVideo()
}

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

    isPlayerStopped = true

    cell.stopVideo()
}

more:

If I completely remove the DispatchQueues and the functions inside of them and use just print statements, the print statement that prints indefinitely print("3. Player is Not Playing... \n") only prints twice, it no longer prints indefinitely so I don't know what's going on with this.

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 {
                print("1. Player is Playing\n")
            }
        } else if keyPath == "timeControlStatus" {

            if player?.timeControlStatus == .playing {
                print("2. Player is Playing\n")

            } else {
                print("3. Player is Not Playing... \n")
            }
        }
    }
}


来源:https://stackoverflow.com/questions/62377105/swift-avplayer-kvo-in-cells-parent-vc-causing-xcode-to-freeze

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!