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 设为局部变量。但这只是额外的优雅,对于示例的重点而言并不是必需的。)
我有一个占据整个屏幕的单元格,所以一次只有 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 设为局部变量。但这只是额外的优雅,对于示例的重点而言并不是必需的。)