3

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

但是,我们在导入大型数据集时遇到了问题。首先让我告诉你,我们的数据模型相当复杂——很多一对多的关系和实体。

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

然而,当我们开始注意到随着我们的数据集变得越来越大时,分析应用程序会告诉我们 Core Data 在主线程上占用了大量的 CPU 时间。切换到NSPersistentContainer似乎可以解决这个问题。主线程上的活动少了很多。我们推测这是因为通过主队列的流量较少(因为NSPersistentContainers 提供的后台队列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 = true并且还建议直接保存到持久存储比保存到父子配置中的中间人(视图上下文)更有效:

两个上下文都连接到相同的 persistentStoreCoordinator,后者作为它们的父级用于数据合并。这比在父上下文和子上下文之间合并更有效。

我们实际上已经设法解决了这个问题,方法是删除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()
        }
    }
...

实际上,这在父子配置中配置了我们的主上下文和子上下文 - 根据 Apple 的说法,这应该是效率较低的。

这行得通!数据正确保存到磁盘(已验证),数据有效(已验证),不再NSFetchedResultsController挂起!

然而,这提出了几个问题:

  • 为什么NSPersistentContainer在处理大型数据集时,Apple 推荐的设置结果的方法是锁定主队列?不是应该更有效率吗?我们缺少什么吗?
  • 有没有人遇到过这样的问题,也许以不同的方式解决了它?我们找不到太多关于设置NSPersistentContainer在线处理大型数据集的信息。
  • 你能看出我们设置堆栈的方式有什么问题吗,或者建议对配置进行改进?
  • 看起来好像直接保存到持久化存储,然后viewContext合并更改效率低于父子配置?有人可能对此有所了解吗?

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

4

0 回答 0