具有大型数据集的NSPersistentContainer和NSFetchedResultsController

时间:2019-05-30 23:31:36

标签: ios nsfetchedresultscontroller nspersistentcontainer

我们最近将应用切换为使用NSPersistentContainer来设置我们的核心数据堆栈。删除样板(例如自动使用保存通知和合并处理)对我们很有吸引力,而且应该设置为非常有效。

但是,导入大型数据集时我们遇到了一个问题。首先,我告诉您我们的数据模型非常复杂-许多一对多的关系和实体。

Core Data堆栈过去被设置为使用附加到NSManagedObjectContext的专用队列NSPersistentStoreCoordinator来对后台队列执行持久性。主队列上下文将成为该上下文的子级;作为主队列上下文的子级创建的专用队列上下文,以处理保存。在NSPersistentContainer发明之前,这是一个相当标准的设置。

但是,当我们开始注意到随着我们的数据集变得越来越大时,对应用程序进行性能分析会显示Core Data在主线程上占用了大量CPU时间。切换到NSPersistentContainer似乎可以解决此问题。主线程上的活动少得多。我们认为这是因为通过主队列的流量减少了(因为NSPersistentContainer所售出的newBackgroundQueue()的后台队列被设置为直接保存到商店协调器;它们不是主队列的子级)队列上下文)。

在数据集增长之前,这似乎很好。我们注意到,当处理大约15,000条记录(有时最多包含10-15,000个与这些记录相关的对象)时,如果设置了NSFetchedResultsController来观察这些对象,则在保存背景上下文时,UI会挂起。不好长达1分钟。显然这是不可取的。

这是我们的持久性容器的设置方式:

...
    public init(storeURL: URL, modelName: String, configureStoreDescriptionHandler: ((NSPersistentStoreDescription, NSManagedObjectModel) -> ())? = nil) throws {
        guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else { throw StackError.modelNotFound }
        guard let model = NSManagedObjectModel(contentsOf: modelURL) else { throw StackError.modelNotCreated }

        let storeDescription = NSPersistentStoreDescription(url: storeURL)
        storeDescription.type = NSSQLiteStoreType

        configureStoreDescriptionHandler?(storeDescription, model)

        storeDescription.shouldMigrateStoreAutomatically = true
        storeDescription.shouldInferMappingModelAutomatically = true
        storeDescription.shouldAddStoreAsynchronously = false

        container = NSPersistentContainer(name: modelName, managedObjectModel: model)
        container.persistentStoreDescriptions = [storeDescription]

        var outError: StackError?
        container.loadPersistentStores { (storeDescription, error) in
            if let error = error {
                assertionFailure("Unable to load \(storeDescription) because \(error)")
                outError = .storeNotMigrated
            }
        }

        if let error = outError {
            throw error
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
    }

    public var mainQueueManagedObjectContext: NSManagedObjectContext {
        return container.viewContext
    }

    public func newPrivateQueueContext() -> NSManagedObjectContext {
        let context = container.newBackgroundContext()
        return context
    }
...

我们通过newPrivateQueueContext()获取私有队列上下文,执行工作,然后保存。大数据集导致NSFetchedResultsController挂起。

Apple建议将viewContext.automaticallyMergesChangesFromParent = trueand also suggests设置为直接保存到持久性存储要比在父子配置中保存最多一个中间人(视图上下文)更有效:

  

两个上下文都连接到相同的persistentStoreCoordinator,该持久性StoreCoordinator用作其父级以进行数据合并。这比合并父子上下文之间的效率更高。

通过删除automaticallyMergesChangesFromParent = true并对配置专用队列上下文的方式进行了以下更改,我们实际上已经设法解决了这个问题:

...
    public var mainQueueManagedObjectContext: NSManagedObjectContext {
        return container.viewContext
    }

    public func newPrivateQueueContext() -> NSManagedObjectContext {
        let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        context.parent = container.viewContext

        NotificationCenter.default.addObserver(self, selector: #selector(handlePrivateQueueContextDidSaveNotification(_:)), name: .NSManagedObjectContextDidSave, object: context)

        return context
    }

    @objc func handlePrivateQueueContextDidSaveNotification(_ note: Notification) {
        container.viewContext.performAndWait {
            try? container.viewContext.save()
        }
    }
...

实际上,这会将我们的主上下文和子上下文配置为父子配置-据苹果称,这应该效率较低。

这有效!数据已正确保存到磁盘(已验证),数据有效(已验证),并且没有其他NSFetchedResultsController挂起!

但是,这引发了一些问题:

  • 为什么苹果公司推荐的设置NSPersistentContainer的方法会导致在处理大型数据集时锁定主队列?它不是应该更有效吗?我们缺少什么吗?
  • 有人遇到过这样的问题,并且可能以其他方式解决了吗?我们找不到太多有关设置NSPersistentContainer以便在线处理大型数据集的信息。
  • 您能看到我们建立堆栈的方式有什么问题吗? 也许建议改进配置?
  • 似乎直接保存到持久性存储中,然后viewContext合并更改的效率不如父子配置?可能有人对此有所启示吗?

我应该补充一点,我们试图通过设置NSFetchedResultsController来提高fetchBatchSize的效率,并改善谓词,但无济于事。

0 个答案:

没有答案