带有核心数据和 NSFetchedResultsController 的后台线程
Posted
技术标签:
【中文标题】带有核心数据和 NSFetchedResultsController 的后台线程【英文标题】:Background thread with Core Data and NSFetchedResultsController 【发布时间】:2014-11-21 17:59:32 【问题描述】:我做了一些研究,发现了一些关于 Objective-C 代码的不错的信息,但对于 Swift 几乎一无所获。我认为这是一个非常常见的模式,所以希望我们可以敲定如何正确地做到这一点。我已经取得了一些相当大的进步,感觉自己已经很接近了,但我对 Swift 的了解还不够。
目标:制作一个使用后台线程来解析数据并执行长提取请求的应用,并拥有一个使用 NSFetchedResults 控制器的主线程。
我的一个函数中的代码用于衍生一个新线程
let tQueue = NSOperationQueue()
let testThread1 = testThread()
tQueue.addOperation(testThread1)
testThread1.threadPriority = 0
testThread1.completionBlock = () -> () in
println("Thread Completed")
我为制作线程而制作的课程
class testThread: NSOperation
var delegate = UIApplication.sharedApplication().delegate as AppDelegate
var threadContext:NSManagedObjectContext?
init()
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
override func main()
self.threadContext = NSManagedObjectContext()
threadContext!.persistentStoreCoordinator = delegate.persistentStoreCoordinator
...
//Code that actually does a fetch, or JSON parsing
...
threadContext!.save(nil)
NSNotificationCenter.defaultCenter().removeObserver(self)
func contextDidSave(notification: NSNotification)
let sender = notification.object as NSManagedObjectContext
if sender !== self.threadContext
self.threadContext!.mergeChangesFromContextDidSaveNotification(notification)
我不会包含 NSFetchedResultsController 的所有代码,但我有一个链接到主上下文。当我的线程被注释掉时,应用程序运行正常,它会阻塞 UI 并解析/获取需要插入核心数据的数据,当全部完成后,UI 将解锁。
当我添加线程时,只要我在 UI 中执行任何可能触发保存到主上下文的操作(在这种情况下,tappedOnSection 表函数执行保存),应用程序就会崩溃并且唯一出现的东西在控制台是。 “lldb”。触发错误的突出显示的行是
managedObjectContext?.save(nil)
旁边的错误是“EXC_BAD_ACCESS(code 1, address=...
如果我只是等待后台线程完成,完成后,我也会收到错误,这一次跟踪到 NSFetchedResultsController 的“didChangeObject”方法。它说“在展开可选值时意外发现 nil,并标记以下情况:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
switch(type)
... other cases
case NSFetchedResultsChangeType.Update:
self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
...other cases
我假设我遇到了一些我没有正确处理的并发问题。我认为监视更改的 NSNotification 可以处理此问题,但我一定遗漏了其他内容。
override func viewDidLoad()
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
...
//Code here calls the function that starts the thread shown previously to do a background fetch
func contextDidSave(notification: NSNotification)
let sender = notification.object as NSManagedObjectContext
if sender !== self.managedObjectContext!
println("Save Detected Outside Thread Main")
self.managedObjectContext!.mergeChangesFromContextDidSaveNotification(notification)
更新:
在你们的帮助下,我已经能够定位错误。似乎来自 NSFetchedResultsController 的 didChangeObject 方法是问题所在。如果数据更改或插入新行,didChange Object 方法会触发相应的方法来执行这些动画,在这里我得到 nil 错误。很明显,关键是当背景数据被获取时,它只会平滑地动画,但不是这样做,而是爆炸。如果我将此功能注释掉,则不会出现任何错误,但也会失去我希望的流畅动画。附加的 didChangeObject 方法如下。它主要来自 NSFetchedResultController 上的 swift 文档:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
switch(type)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Update:
if self.tableView.cellForRowAtIndexPath(indexPath!) != nil
self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
case NSFetchedResultsChangeType.Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
【问题讨论】:
您应该在调用self.configureCell...
之前检查self.tableView.cellForRowAtIndexPath
是否为nil - 如果相关行不再可见,则它可以为nil。
这似乎有助于解决我遇到的一些错误,但在主上下文和后台上下文同时进行保存时,我仍然遇到并发错误,但它没有没有让应用程序崩溃,它只是把 UI 弄乱了大约 15 秒,然后它神奇地修复了自己。我需要在 contextDidSave 方法中执行 tableView.reloadData 吗?
FRC 委托方法应该对 tableView 进行必要的更改,但考虑到您可能有许多来自后台的更新,您可以在 controllerDidChangeContent:
方法中放置一个 reloadData。
与你的崩溃无关,但是......“但我一定是错过了其他东西。”是的。您正在使用队列限制。不要使用带有队列限制的合并通知来传达上下文之间的更改,而是使用上下文嵌套。合并通知适用于线程限制,但由于许多原因不能与队列限制一起正常工作。另请参阅:quellish.tumblr.com/post/93190211147/… 和 quellish.tumblr.com/post/97430076027/…
我认为您在didChangeObject
中仍然存在一些indexPath 问题:在.Update
的情况下,您测试cellForRowAtIndexPath(indexPath!) != nil
,但将cellForRowAtIndexPath(newIndexPath!)
传递给configureCell
。你应该使用indexPath!
。同样在 .Move 案例中,您使用indexPath!
进行删除和插入:您应该从indexPath!
删除并在newIndexPath!
插入。此外,在您的numberOfRowsInSection
中,您需要访问部分中的特定元素!当前部分的数组:与第 226 行和第 227 行的 viewForHeaderInSection
完全相同。
【参考方案1】:
最后,我研究了许多不同的线程处理方法。最有用的是多上下文,通知中心如here 所述,我也实现了多上下文解决方案,但最终降级为另一个。我的问题原来是我在多个 NSFetchedResultsControllers 之间共享一个委托,而没有检查传入的控制器是否与表当前使用的控制器相同。每当数据自动重新加载时,这都会产生超出范围的错误。
我的后台线程解决方案很简单。
-
创建主上下文
创建背景上下文。
使用 performBlock 调用后台上下文
context.performBlock
//background code here
监听主要上下文的变化。
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
func contextDidSave(notification: NSNotification)
let sender = notification.object as NSManagedObjectContext
if sender != managedObjectContext
managedObjectContext!.mergeChangesFromContextDidSaveNotification(notification)
将更改合并到 mainContext。
我的初始设置实际上非常接近正确,我不知道的是,当你设置你的 backgroundContext 时,你可以给它一个并发类型
let childContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
然后你可以在后台线程中使用 context.performBlock 调用上下文(如上所示),只要你想做后台线程的事情。
此外,NSFetchedResultsController 使用 mainContext 作为它们的上下文,这样它们就不会在解析过程中被阻塞。
更新
做后台线程有多种方式,上面的解决方案只是其中一种。 Quellish 描述了另一种流行的方法in his article here。它信息量很大,我推荐它,它描述了队列限制的嵌套上下文方法。
【讨论】:
您的解决方案使用队列限制,但不使用嵌套上下文来传达更改?您的 NSManagedObjectContextDidSaveNotification 将到达后台上下文的队列。您的合并将发生在错误的队列中。出于这个(以及许多其他!)原因,在使用队列限制时,最好使用嵌套上下文来传达更改。 @quellish,我最终实现了您所描述的嵌套上下文版本和通知合并版本。由于本文floriankugler.com/blog/2013/4/29/… 中描述的性能爱好者,我的团队倾向于使用通知版本。然而,两者都没有任何错误。我遇到错误的主要原因是我们在共享单个委托之间切换的 3 个获取结果控制器。我编辑了我的解决方案以引用嵌套上下文,以帮助那些有类似问题的人朝着正确的方向发展。 如果您使用带有嵌套上下文的通知,您将难以解决错误。您正在使用两种相互冲突的方法来传达上下文之间的更改。您真的能够使用 Instruments 衡量性能问题吗? 我自己没有执行任何操作,只是我从那篇文章中读到的。我很好奇他的陈述是否属实,如果不是,我肯定会另辟蹊径。就像我说的那样,我也有一个使用该方法的工作分支,所以如果我们遇到问题,我将切换回嵌套上下文,这是一个非常简单的修复。如果我有任何问题,我会发布。【参考方案2】:您应该在调用self.configureCell...
之前检查self.tableView.cellForRowAtIndexPath
是否为nil - 如果相关行不再可见,则它可以为nil。
FRC 委托方法应该对 tableView 进行必要的更改,但考虑到您可能有许多来自后台的更新,您可以在 controllerDidChangeContent:
方法中放置一个 reloadData
。
【讨论】:
再次感谢,我可以发誓这是我的线程问题,但它最终成为更多 FetchedResultController 问题。谢谢! 抱歉,它又坏了。现在使用相同方法的 insertRow。我所做的是从模拟器中删除应用程序,以便它必须将所有内容重新加载到核心数据中,我假设 didChangeObject 方法的全部目的是在您编辑或插入行时提供流畅的动画。好吧,每当我编辑或插入一行时,它都会引发错误。有什么想法吗? 你应该在newIndexPath
插入,从indexPath
删除。移动也是如此。
我用 newIndexPath 交换了 indexPath 并且更新的行不会出现,除非我再次重新加载应用程序。
didChangeObject 方法不起作用,但我认为 quellish 是在正确的轨道上。我需要使用上下文嵌套来解决这个问题,通知的东西不好用。以上是关于带有核心数据和 NSFetchedResultsController 的后台线程的主要内容,如果未能解决你的问题,请参考以下文章
iOS - 将单元格添加到带有核心数据和 NSFetchedResultsController 的空 UITableView 部分
使用 NSFetchedResultsController 在分段表视图中排序和显示数据