Swift - 使用两个不同的 OperationQueue 和 KVO 时应用程序崩溃
Posted
技术标签:
【中文标题】Swift - 使用两个不同的 OperationQueue 和 KVO 时应用程序崩溃【英文标题】:Swift - Application crash when using two different OperationQueues with KVO 【发布时间】:2018-05-26 19:07:00 【问题描述】:我使用 JSON 获取两种类型的信息,并使用 addObserver(forKeyPath:"operations"...) 将“操作”添加到 2 个不同的操作队列类。
在函数 observeValue 中,我正在检查 operationQueue1.operations.isEmpty 是否,然后在 UI 中刷新我的信息。我对 operationQueue2 的 if else 做同样的事情,但是当两个操作在某个时候启动时,应用程序崩溃并显示错误消息:*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <AppName.ViewController 0x102977800> for the key path "operations" from <AppName.OperationQueue1 0x1c4a233c0> because it is not registered as an observer
。
仅启动 1 次操作时我没有问题。有什么建议吗?
func getInfo1()//runned in viewDidLoad
operationQueue1.addObserver(forKeyPath:"operations"...)
operationQueue1.dataTask(URL:"..."....)
DispatchQueue.main.async
NotificationCenter.default.postNotification(NSNotification.Name(rawValue: "NewDataReceived1", userInfo:infoFromTheWebsite)
func NewDataReceived1()
here I add the information to arrays to be loaded in tableView1
HERE IS THE CODE FOR 2ND INFO WHICH IS THE SAME
override func observeValue(forKeyPath keyPath: String?, ....)
if(object as? operationQueue1 == operationQueue1Class && keyPath == "operations" && context == context1)
if(operationQueue1.operations.isEmpty)
DispatchQueue.main.async
operationQueue1..removeObserver(self, forKeyPath:"operations")
Timer.scheduled("refreshingTableInformation1")
else if(operationQueue2....)
SAME AS OPERATION 1, BUT USING DIFFERENT FUNC TO REFRESH TABLE INFORMATION AND THE TABLES ARE DIFFERENT
else
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
func refreshingTableInformation1()
tableView1.reloadData()
Timer.scheduled("getInfo1", repeat:false)
func refreshingTableInformation2()
tableView2.reloadData()
Timer.scheduled("getInfo2", repeat:false)
有时工作 10 秒后崩溃,有时工作超过 60 秒然后崩溃...
【问题讨论】:
就像错误所说的那样,即使视图控制器未注册为观察者,您也试图从观察OperationQueue1
的操作键路径中删除视图控制器。如果您更改了对 vc 的引用,则可能会发生这种情况。发布您的操作队列的代码以及添加观察者的位置。
我在 2 个请求之前添加了观察者,它们被锁定在无限循环中。当我更新 UI 中的信息时,我再次设置了观察者,并开始请求再次获取信息。
您的代码不是很清楚。无论如何,为什么不在收集“NewDataReceived1”的数组上放置一个观察者呢?会更容易处理。此外,如果您有同步问题,最好使用诸如 Alamofire 之类的库来处理您的 http 请求。这样可以省去很多麻烦。
我不会在 NewDataReceived1 中添加观察者,因为我正在使用 operationQueue1.dataTask 3 次,这意味着我要设置 3 个观察者...
【参考方案1】:
您的代码容易受到竞争条件的影响。考虑以下场景:
调用getInfo1()
,向operationQueue1
添加一个操作。
操作完成,这意味着您的 KVO 观察被调用。队列现在是空的,因此您的观察计划在主调度队列中移除您的观察者。
现在,在您提交到主队列的操作能够运行之前,调用 getInfo1()
,它会向 operationQueue1
添加一个新操作,该操作在您在步骤中排队的操作之前完成2 有机会运行(嘿,也许主队列正忙于某些事情;这很容易发生,因为它是一个串行队列)。
您观察到第一次调用getInfo1()
在队列为空时被调用再次,导致另一个 取消注册块提交到主队列。
这两个注销块最终在主队列上执行。第二个使程序崩溃,因为您已经注销了观察者。
您可以改用 Swift 4 的基于块的观察者并将观察者设置为 nil
而不是显式注销它来解决这个问题(假设代码没有更多这种性质的问题)。但是,我建议 KVO 是 wrong tool 用于您正在尝试做的事情。就像老《水晶探秘》游戏的说明书上说的,有点像用高射炮杀蚊子。
从我从上面的代码中可以看出,您使用 KVO 似乎只是为了安排您提交到队列的操作或一组操作何时完成的通知。根据您的 dataTask
方法实际执行的操作,我将执行以下操作:
如果您只提交一个操作:将操作的 completionBlock
属性设置为刷新表信息的闭包。
如果您提交多个操作:创建一个新的 BlockOperation
以刷新您的表信息,并在该操作上调用 addDependency
以及您提交到队列的所有其他操作。然后,提交该操作。
这将为您提供一种更清洁、更轻松的方式来监控您的任务完成情况。而且由于您不再需要完全清空队列,您甚至可能不再需要使用两个单独的队列,具体取决于您对它们进行的其他操作。
【讨论】:
我正在使用 KVO,因为我有动态表格视图,如果网站没有返回我的信息,我没有创建表格视图 @BogdanBogdanov 您能否更具体地说明您在做什么?我不明白为什么推迟表视图的创建需要在操作队列上使用 KVO,而不是仅仅在完成操作中进行。 我同意@CharlesSrstka 的观点,我的第一个想法是比赛条件,尤其是他上面列表中的第 4 位。使用较新的tableView.beginUpdates()
和.endUpdates()
或performBatchUpdates(_:completion:)
可以从block-observer 的changeHandler 更新tableView。请注意,observe(_:options:changeHandler:)
会在需要时调用 addObserver_:forKeyPath:options:context:)
和 removeObserver(_:forKeyPath:context:)
。以上是关于Swift - 使用两个不同的 OperationQueue 和 KVO 时应用程序崩溃的主要内容,如果未能解决你的问题,请参考以下文章
583. Delete Operation for Two Strings
Swift - 使用两个不同的 OperationQueue 和 KVO 时应用程序崩溃
如何使用 Swift 将具有两个不同键的两个值存储到数组或字典中? [关闭]