我们最近将我们的应用程序切换到使用NSPersistentContainer
来设置我们的核心数据堆栈。删除样板文件(例如自动使用保存通知和合并处理)对我们很有吸引力,而且它应该设置得非常高效。
但是,我们在导入大型数据集时遇到了问题。首先让我告诉你,我们的数据模型相当复杂——很多一对多的关系和实体。
Core Data 堆栈过去被设置为使用NSManagedObjectContext
附加到的私有队列NSPersistentStoreCoordinator
来执行后台队列的持久性。主队列上下文将是该上下文的子上下文;作为主队列上下文的子级创建的私有队列上下文以处理保存。在发明之前建立的相当标准NSPersistentContainer
。
然而,当我们开始注意到随着我们的数据集变得越来越大时,分析应用程序会告诉我们 Core Data 在主线程上占用了大量的 CPU 时间。切换到NSPersistentContainer
似乎可以解决这个问题。主线程上的活动少了很多。我们推测这是因为通过主队列的流量较少(因为NSPersistentContainer
s 提供的后台队列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
,但无济于事。