Swift -单元格 parent vc 中的 AVPlayer' KVO 导致 Xcode 冻结

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

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

3 个问题:

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

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

3- 在 KVO 中,我尝试使用通知发送到单元格而不是调用 cell.stopVideo() 函数,但单元格内的函数从未运行。

我该如何解决这个问题?

应该注意的是,除了 KVO 不工作之外,其他一切都正常。我有一个周期性的时间观察器,每当我滚动时,它都能完美地运行每个单元格,视频加载正常,当我按下单元格时 stop/play 视频一切正常。

单元格:

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
}

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

在 KVO 中,第 3 个永远打印,我删除了 DispatchQueue 并添加了 stopVideoInCell() 函数。当函数调用cell的stopVideo()函数时,player?.pause短暂得到一个EXC_BAD_ACCESS(code=2,address=0x16b01ff0)崩溃。

应用随后终止。在控制台内,唯一打印的是:

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

当我去终端查看打印出来的东西时,我唯一得到的是一堆 lldb-rpc-server_2020-06-14-155514_myMacName.crash 语句,这些语句来自我 运行 陷入这次崩溃的所有日子。

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

我使用 Boolean 解决了这个问题。这不是最优雅的答案,但它确实有效。如果有人能提出更好的答案,我会接受。由于我放在 more:

下,所以发生了什么没有意义

答案:

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

更多:

如果我完全删除 DispatchQueues 和其中的函数并只使用 print 语句,无限期打印的 print 语句 print("3. Player is Not Playing... \n") 只打印两次,它不再无限期地打印所以我不知道是什么继续这个。

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

问题在于,在您的 属性 观察者中,您正在对您正在观察的 属性 进行更改。那就是恶性循环,无限递归; 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的代码中间!所以我们再做一次,它又发生了。又一次,它又发生了。无限循环...

你将如何摆脱困境?你需要以某种方式打破递归。我可以想到两种明显的方法。一种是自己思考,"Well, I only care about a switch from On to Off. I don't care about the other way." 假设这是真的,你可以用一个简单的 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 设为局部变量。但这只是额外的优雅,对于示例的重点而言并不是必需的。)