带有核心数据和 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 部分

带有 sqlite 的核心数据仅返回 3 行

带有核心数据的 iOS Today 扩展

使用 NSFetchedResultsController 在分段表视图中排序和显示数据

带有 UIDocument 的核心数据:新对象的关系在 didEnterBackground 和重新打开后被破坏

在自定义对象类型的核心数据中保存带有子项的类别