ios - 单元格的父 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 会冻结。
解决方案
问题是,在您的属性观察者中,您正在对正在观察的属性进行更改。那是一个恶性循环,一个无限递归;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 设置为该配置中的局部变量。但这只是一种额外的优雅,对于示例而言并不重要。)
推荐阅读
- javascript - 如何根据 JavaScript 中的自定义规则加入数组中的对象
- java - jsp下拉菜单没有自动选择正确的值
- c - 无锁并发写入 O_APPEND 文件
- javascript - 我可以使用 dnnModal.show 将类、ID 或数据属性传递给 DNN 模态吗
- selenium-webdriver - 获取 wdio 实例线程号
- sql-server - ETL | 使用 SSIS 从 SQL 到 Excel,特定格式
- c++ - 无法使用 libtool 将 -shared 参数传递给 g++
- reactjs - 如何检查给定库的重量?
- sql-server - 在插入语句中将 nvarchar 转换为数据类型 numeric 的算术溢出错误
- python - 如何以编程方式更改用户的密码而无需 Python 中的交互式提示?