【问题标题】:Update NSFetchedResultsController using performBackgroundTask使用 performBackgroundTask 更新 NSFetchedResultsController
【发布时间】:2016-11-30 15:28:41
【问题描述】:

我有一个NSFetchedResultsController,我正在尝试在后台上下文中更新我的数据。例如,这里我试图删除一个对象:

persistentContainer.performBackgroundTask { context in
  let object = context.object(with: restaurant.objectID)
  context.delete(object)
  try? context.save()
}

有两件事我不明白:

  1. 我希望这会修改,但不保存父上下文。但是,肯定会保存父上下文(通过手动打开 SQLite 文件进行验证)。
  2. 我原以为NSFetchedResultsController 会在后台内容保存到其父级时进行更新,但这并没有发生。我需要在主线程上手动触发一些东西吗?

显然有些东西我没有得到。谁能解释一下?

我知道我已经正确实现了获取结果控制器委托方法,因为如果我将代码更改为直接更新viewContext,一切都会按预期工作。

【问题讨论】:

    标签: ios swift core-data


    【解决方案1】:

    说明

    NSPersistentContainer 的实例方法 performBackgroundTask(_:)newBackgroundContext() 记录不充分。

    无论您调用哪种方法,在任何一种情况下,(返回的)临时NSManagedObjectContext 都使用privateQueueConcurrencyType 设置并直接与NSPersistentStoreCoordinator 关联,因此没有parent

    documentation:

    调用此方法会导致持久化容器创建并 返回一个新的 NSManagedObjectContext 并将 concurrencyType 设置为 私有队列并发类型。这个新的上下文将与 NSPersistentStoreCoordinator 直接并设置为使用 NSManagedObjectContextDidSave 自动广播。

    ...或自己确认:

    persistentContainer.performBackgroundTask { (context) in
        print(context.parent) // nil
        print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)
    }
    
    let context = persistentContainer.newBackgroundContext()
    print(context.parent) // nil
    print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)
    

    由于缺少parent,更改不会提交给parent context,例如viewContextviewContext 未触及,连接的 NSFetchedResultsController 不会识别任何更改,因此不会更新或调用其 delegate 的方法。相反,更改将直接推送到persistent store coordinator,然后保存到persistent store

    我希望我能够为您提供帮助,如果您需要进一步的帮助,我可以在我的回答中添加如何获得您所描述的所需行为。 解决方案添加在下方)

    解决方案

    您通过使用两个具有父子关系的NSManagedObjectContexts 来实现您所描述的行为:

    // Create new context for asynchronous execution with privateQueueConcurrencyType  
    let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    // Add your viewContext as parent, therefore changes are pushed to the viewContext, instead of the persistent store coordinator
    let viewContext = persistentContainer.viewContext
    backgroundContext.parent = viewContext
    backgroundContext.perform {
        // Do your work...
        let object = backgroundContext.object(with: restaurant.objectID)
        backgroundContext.delete(object)
        // Propagate changes to the viewContext -> fetched results controller will be notified as a consequence
        try? backgroundContext.save()
        viewContext.performAndWait {
            // Save viewContext on the main queue in order to store changes persistently
            try? viewContext.save()
        }
    }
    

    不过,您也可以坚持使用performBackgroundTask(_:) 或使用newBackgroundContext()。但如前所述,在这种情况下,更改会直接保存到持久存储中,默认情况下不会更新 viewContext。为了将更改向下传播到viewContext,从而通知NSFetchedResultsController,您必须将viewContext.automaticallyMergesChangesFromParent 设置为true

    // Set automaticallyMergesChangesFromParent to true
    persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
    persistentContainer.performBackgroundTask { context in
        // Do your work...
        let object = context.object(with: restaurant.objectID)
        context.delete(object)
        // Save changes to persistent store, update viewContext and notify fetched results controller
        try? context.save()
    }
    

    请注意,一次添加 10.000 个对象等大量更改可能会使您的 NSFetchedResultsController 发疯,从而阻止 main queue

    【讨论】:

    • 感谢您的解释。如果您可以添加我将如何在后台线程上更新数据,然后仅在我完成后台更新并保存到父上下文(主线程)时更新获取的结果控制器,那将非常感激!
    • @ganzogo,您是否已经注意到添加的解决方案部分?有帮助吗?
    • automaticMergesChangesFromParent 解决方案是 Apple 似乎正在推动的。实际上在文档中,这个想法是默认情况下 automaticMergesChangesFromParent 有点正确,但似乎并非如此,并且需要布尔值。文档指定 viewContext 现在已生成并且应该是只读的:“与主队列关联的托管对象上下文。(只读),此上下文直接与 NSPersistentStoreCoordinator 关联,默认情况下是非分代的。”
    • 很抱歉提出一个旧线程,但这仍然是相关的。当使用NSPersistentContainer 在后台上下文中保存 10,000 多个对象时,有没有办法阻塞主队列?我知道我们可以手动设置堆栈并将后台上下文链接到存储协调器,但是,我还没有找到使用容器实现此目的的方法。有人处理过这个吗?
    • @JohnRogers 您是否找到了解决方案,如何不阻止主队列保存大量数据以使用 NSPersistentContainer 存储协调器?您是否尝试为“viewContext”创建和设置“父”私有上下文(带有存储协调器)并将“viewContext”的存储协调器设置为 nil ?
    【解决方案2】:

    除非您将视图上下文设置为自动合并来自父级的更改,否则视图上下文不会更新。 viewContext 已设置为您从 NSPersistentContainer 收到的任何 backgroundContext 的子级。

    尝试只添加这一行:

    persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
    

    现在,viewContext 将在 backgroundContext 保存后更新,这将触发 NSFetchedResultsController 更新。

    【讨论】:

    • 我准确地设置了这一行,但视图上下文不会更新。表视图在重新启动时显示数据。单元测试代码也可以。令人沮丧,所以当我弄清楚我做错了什么时,我会发布。
    • @Christopher,请确保在更新之前设置 tableView 数据源。此外,在 fetchedresultscontroller 的其他方法中设置断点,以确保控制器看到更新。另外,请确保正确设置 NSFetchedResultsController。
    • 谢谢。当天晚些时候开始工作。 Git 日志显示我多次调用“newBackgroundContext()”,因此没有正确保存。
    【解决方案3】:

    这在我的项目中非常适合我。 在函数 updateEnglishNewsListener(:) 中,这里的参数数据在任意对象中,我进一步将其转换为 json 格式以保存。

    Core Data 使用线程(或序列化队列)限制来保护托管对象和托管对象上下文(请参阅 Core Data 编程指南)。这样做的结果是,上下文假定默认所有者是分配它的线程或队列——这由调用其 init 方法的线程确定。因此,您不应该在一个线程上初始化上下文,然后将其传递给另一个线程。

    一共有三种 1. ConfinementConcurrencyType 2. PrivateQueueConcurrencyType 3. MainQueueConcurrencyType

    MainQueueConcurrencyType 创建一个与主队列关联的上下文,非常适合与 NSFetchedResultsController 一起使用。

    在 updateEnglishNewsListener(:) 函数中,参数数据是您的输入。 (数据->您要更新的数据。)

     private func updateEnglishNewsListener(data: [AnyObject] ){
    
                //Here is your data
    
                let privateAsyncMOC_En = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
                // The context is associated with the main queue, and as such is tied into the application’s event loop, but it is otherwise similar to a private queue-based context. You use this queue type for contexts linked to controllers and UI objects that are required to be used only on the main thread.
    
                       privateAsyncMOC_En.parent = managedObjectContext
                        privateAsyncMOC_En.perform{
                            // The perform(_:) method returns immediately and the context executes the block methods on its own thread. Here it use background thread.
    
                            let convetedJSonData = self.convertAnyobjectToJSON(anyObject: data as AnyObject)
                            for (_ ,object) in convetedJSonData{
                                self.checkIFNewsIdForEnglishAlreadyExists(newsId: object["news_id"].intValue, completion: { (count) in
    
                            if count != 0{
                                    self.updateDataBaseOfEnglishNews(json: object, newsId: object["news_id"].intValue)
                                }
                            })
                        }
                        do {
                            if privateAsyncMOC_En.hasChanges{
    
                            try privateAsyncMOC_En.save()
    
                        }
                        if managedObjectContext.hasChanges{
    
                            try managedObjectContext.save()
    
                        }
    
                    }catch {
                        print(error)
                    }
                }
        }
    

    检查coredata中是否已经存在数据以避免冗余数据。 Coredata 没有主键概念,所以我们依次检查数据是否已经存在于 coredata 中。当且仅当更新数据已存在于 coredata 中时,才会更新数据。这里 checkIFNewsIdForEnglishAlreadyExists(:) 函数返回 0 或 value 。如果它返回 0,则数据不保存在数据库中,否则保存。我正在使用完成句柄来了解新数据或旧数据。

        private func checkIFNewsIdForEnglishAlreadyExists(newsId:Int,completion:(_ count:Int)->()){
    
            let fetchReq:NSFetchRequest<TestEntity> = TestEntity.fetchRequest()
            fetchReq.predicate = NSPredicate(format: "news_id = %d",newsId)
            fetchReq.fetchLimit = 1 // this gives one data at a time for checking coming data to saved data
    
            do {
                let count = try managedObjectContext.count(for: fetchReq)
                completion(count)
    
            }catch{
                let error  = error as NSError
                print("\(error)")
                completion(0)
            }
    
    
        }
    

    根据需要将旧数据替换为新数据。

        private func updateDataBaseOfEnglishNews(json: JSON, newsId : Int){
    
            do {
                let fetchRequest:NSFetchRequest<TestEntity> = TestEntity.fetchRequest()
    
                fetchRequest.predicate = NSPredicate(format: "news_id = %d",newsId)
    
    
                let fetchResults = try  managedObjectContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as? [TestEntity]
                if let fetchResults = fetchResults {
    
                    if fetchResults.count != 0{
                        let newManagedObject = fetchResults[0]
                        newManagedObject.setValue(json["category_name"].stringValue, forKey: "category_name")
                        newManagedObject.setValue(json["description"].stringValue, forKey: "description1")
    
                        do {
                            if ((newManagedObject.managedObjectContext?.hasChanges) != nil){
    
                                try newManagedObject.managedObjectContext?.save()
    
                            }
    
                        } catch {
                            let saveError = error as NSError
                            print(saveError)
                        }
                    }
    
                }
    
            } catch {
    
                let saveError = error as NSError
                print(saveError)
            }
        }
    

    将任何对象转换为 JSON 以保存在 coredata 中

        func convertAnyobjectToJSON(anyObject: AnyObject) -> JSON{
            let jsonData = try! JSONSerialization.data(withJSONObject: anyObject, options: JSONSerialization.WritingOptions.prettyPrinted)
            let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String
            if let dataFromString = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: false) {
                let json = JSON(data: dataFromString)
                return json
            }
            return nil
        }
    

    希望对您有所帮助。如果有任何困惑,请询问。

    【讨论】:

    • 感谢您的回答。那么您是说不能使用 PrivateQueueConcurrency 类型进行更新,并且这种类型的上下文只能用于读取吗?如果我像您所做的那样更改为使用 MainQueueConcurrencyType,那么一切都会按预期工作。但是,我试图尽可能长时间地远离主线程。
    • 私有队列并发类型在后台线程中完成所有工作。非常适合处理或磁盘 io。主队列类型只是在 UIThread 上执行所有操作。当您需要执行诸如将 NSFetchedResultsController 绑定到它或需要与处理该上下文对象交织的任何其他与 ui 相关的任务时,这是必要的。并且 MainQueueConcurrencyType 与 ui 元素相关联,记住我使用了创建后台线程的 perform(_:) 函数。
    • 我已经说过“perform(_:) 方法立即返回,上下文在自己的线程上执行块方法。这里它使用后台线程”
    • 要记住的重要一点是,当跨多个线程与托管对象上下文交互时,发送到上下文的所有消息都必须通过 -performBlock: 或 performBlockAndWait: 发送。在最近的一个项目中,我有一个父 NSManagedObjectContext,它支持使用 NSMainQueueConcurrencyType 创建的 NSFetchedResultsController。 ...下一条评论..
    • ...continue...从那里我创建了一个带有 NSPrivateQueueConcurrencyType 的 NSManagedObjectContext 并使用 NSMainQueueConcurrencyType 作为父级设置上下文。现在,当添加一个新对象时,我的子上下文可能包含可丢弃的编辑,该对象最终会出现在由父上下文支持的 NSFetchedResultsController 支持的表视图中。
    猜你喜欢
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-23
    • 1970-01-01
    • 2012-02-17
    • 1970-01-01
    相关资源
    最近更新 更多