我们最近将应用切换为使用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 = true
,and 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
的效率,并改善谓词,但无济于事。