Мы недавно переключили наше приложение на использование NSPersistentContainer
для настройки нашего стека базовых данных. Нам было очень интересно удалить шаблон, такой как автоматическое использование уведомлений о сохранении и обработка слияний, плюс он должен быть настроен на очень высокую эффективность.
Однако мы сталкиваемся с проблемой при импорте больших наборов данных. Позвольте мне начать с того, что я скажу вам, что наша модель данных довольно сложна - множество взаимосвязей и сущностей «один ко многим».
Базовый стек данных раньше был настроен на использование частной очереди NSManagedObjectContext
, присоединенной к NSPersistentStoreCoordinator
, для выполнения сохранения в фоновой очереди. Основной контекст очереди был бы потомком этого контекста; контексты приватной очереди, созданные как дочерние элементы основного контекста очереди для обработки сохранений. Довольно стандартная настройка до изобретения NSPersistentContainer
.
Однако, когда мы начали замечать, что по мере увеличения наших наборов данных, профилирование приложения показало бы нам, что Core Data занимают много процессорного времени в главном потоке. Переход на NSPersistentContainer
, казалось, исправил это. Намного меньше активности в основном потоке. Мы предполагаем, что это связано с меньшим трафиком, проходящим через основную очередь (поскольку фоновые очереди NSPersistentContainer
, представленные newBackgroundQueue()
, настроены для сохранения непосредственно в координатор хранилища; они не являются дочерними элементами основного контекста очереди).
Казалось, что все хорошо, пока набор данных не вырос. Мы заметили, что при обработке около 15 000 записей (иногда до 10-15 000 объектов, связанных с этими записями) при сохранении фонового контекста, если для наблюдения за этими объектами был установлен NSFetchedResultsController
, пользовательский интерфейс зависал. Плохо. До 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
зависает!
Это, однако, поднимает несколько вопросов:
- Почему рекомендуемый Apple способ установки
NSPersistentContainer
приводит к блокировке основной очереди при обработке больших наборов данных? Разве это не должно быть более эффективным? Мы что-то упускаем?
- Кто-нибудь сталкивался с подобной проблемой и, возможно, решил ее по-другому? Мы не можем найти много информации о настройке
NSPersistentContainer
для обработки мега-наборов данных онлайн.
- Можете ли вы увидеть какие-либо проблемы с тем, как мы настроили наш стек, и
возможно предложить улучшения конфигурации?
- Похоже, что при сохранении непосредственно в постоянное хранилище
viewContext
объединение изменений менее эффективно, чем конфигурация родительский-дочерний? Может быть, кто-то может пролить свет на это?
Я должен добавить, что мы попытались сделать нашу NSFetchedResultsController
более эффективной, установив fetchBatchSize
и улучшив предикаты, но безрезультатно.