NSPersistentContainer & NSFetchedResultsController с большим набором данных - PullRequest
1 голос
/ 31 мая 2019

Мы недавно переключили наше приложение на использование 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 и улучшив предикаты, но безрезультатно.

...