带有Core Data和NSFetchedResultsController的后台线程

时间:2014-11-21 17:59:33

标签: ios multithreading core-data swift nsfetchedresultscontroller

我做了一些研究,并在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&#34 ;.突出显示触发错误的行是

    managedObjectContext?.save(nil)

它旁边的错误是" EXC_BAD_ACCESS(代码1,地址= ...

如果我只是等待后台线程完成,完成后,我也会收到一个错误,这次跟踪到" didChangeObject" NSFetchedResultsController的方法。它说"在展开可选值时意外地发现了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的快速文档:

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)
        }
    }

2 个答案:

答案 0 :(得分:9)

最后,我最终研究了许多不同的线程方法。最有用的是多个上下文,其中包含了here所述的通知中心。我还实现了多个上下文解决方案,但最终还是降级到了另一个。我的问题结果是我在多个NSFetchedResultsControllers之间共享一个委托,而没有检查传入的控制器是否与表当前使用的控制器相同。每当数据自动重新加载时,这都会产生超出界限的错误。

我的后台线程解决方案很简单。

  1. 创建主要上下文
  2. 创建背景上下文。
  3. 使用performBlock

    调用背景上下文
    context.performBlock {
       //background code here
    
  4. 使用主要上下文监听更改。

    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)
        }
    }
    
  5. 将更改合并到mainContext中。

  6. 我的初始设置实际上非常接近正确,我不知道的是,当你设置backgroundContext时,你可以给它一个并发类型

    let childContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    

    然后,只要你想做后台线程的东西,就可以使用context.performBlock(如上所示)在后台线程中调用上下文。

    此外,NSFetchedResultsController使用mainContext作为其上下文,以便在解析期间不会阻止它们。

    <强>更新

    有多种方法可以执行后台线程,上面的解决方案只有一种。 Quellish描述了另一种流行的方法in his article here。它非常有用,我推荐它,它描述了队列限制的嵌套上下文方法。

答案 1 :(得分:2)

在调用self.tableView.cellForRowAtIndexPath之前,您应该检查self.configureCell...是否为零 - 如果相关行不再可见,则可以为零。

FRC委托方法应该对tableView进行必要的更改,但鉴于您可能有很多来自后台的更新,您可以在reloadData方法中添加controllerDidChangeContent: