首页 文章

带有Core Data和NSFetchedResultsController的后台线程

提问于
浏览
4

我做了一些研究,并在Objective-C代码上找到了一些不错的信息,但对于Swift几乎没有 . 我认为这是一个非常常见的模式,所以希望我们能够确定如何正确地做到这一点 . 我取得了一些非常重要的进步,感觉我非常接近,但我只是在Swift的深度 .

Goal: 创建一个使用后台线程解析数据并执行长读取请求的应用程序,并使用一个使用NSFetchedResults控制器的主线程 .

我的一个函数中的代码来分拆一个新的Thread

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(代码1,地址= ...

如果我只是等待后台线程完成,完成后,我也会收到一个错误,这次跟踪到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)
    }

}

UPDATE:

在你们的帮助下,我已经能够将错误本地化了 . 似乎来自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)
        }
    }

2 回答

  • 2

    最后,我最终研究了许多不同的线程方法 . 最有用的是多个上下文,如上所述,通知中心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作为其上下文,以便在解析期间不会阻止它们 .

    UPDATE

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

  • 9

    你应该在调用 self.configureCell... 之前检查 self.tableView.cellForRowAtIndexPath 是否为nil - 如果相关的行不再可见,它可以是nil .

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

相关问题