Реализация быстрого и эффективного импорта основных данных в iOS 5

101

Вопрос : Как мне заставить мой дочерний контекст видеть изменения, сохраняющиеся в родительском контексте, чтобы они запускали мой NSFetchedResultsController для обновления пользовательского интерфейса?

Вот установка:

У вас есть приложение, которое загружает и добавляет много XML-данных (около 2 миллионов записей, каждая примерно размером с обычный абзац текста) .sqlite-файл становится размером около 500 МБ. Добавление этого контента в Core Data требует времени, но вы хотите, чтобы пользователь мог использовать приложение, пока данные загружаются в хранилище данных постепенно. Перемещение больших объемов данных должно быть невидимым и незаметным для пользователя, чтобы не было зависаний и дрожания: прокручивается как масло. Тем не менее, приложение тем полезнее, чем больше в него добавляется данных, поэтому мы не можем вечно ждать, пока данные будут добавлены в хранилище Core Data. В коде это означает, что я действительно хотел бы избежать кода, подобного этому, в коде импорта:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

Приложение поддерживает только iOS 5, поэтому самое медленное устройство, которое оно должно поддерживать, - это iPhone 3GS.

Вот ресурсы, которые я использовал до сих пор для разработки моего текущего решения:

Руководство Apple по программированию основных данных: эффективный импорт данных

  • Используйте пулы автозапуска, чтобы уменьшить объем памяти
  • Стоимость отношений. Импортируйте плоский, а в конце исправляйте отношения
  • Не спрашивайте, можете ли вы помочь, это замедляет работу на O (n ^ 2)
  • Импорт пакетами: сохранение, сброс, слив и повтор
  • Отключить диспетчер отмены при импорте

iDeveloper TV - Производительность основных данных

  • Используйте 3 контекста: типы контекста Master, Main и Confinement

iDeveloper TV - Обновление Core Data для Mac, iPhone и iPad

  • Выполнение сохранений в других очередях с performBlock ускоряет работу.
  • Шифрование замедляет работу, отключите его, если можете.

Маркус Зарра, Импорт и отображение больших наборов данных в основных данных

  • Вы можете замедлить импорт, дав время текущему циклу выполнения, чтобы пользователь чувствовал себя комфортно.
  • Пример кода доказывает, что можно выполнять большой импорт и поддерживать отзывчивость пользовательского интерфейса, но не так быстро, как с 3 контекстами и асинхронным сохранением на диск.

Мое текущее решение

У меня есть 3 экземпляра NSManagedObjectContext:

masterManagedObjectContext - это контекст, который имеет NSPersistentStoreCoordinator и отвечает за сохранение на диск. Я делаю так, чтобы мои сохранения были асинхронными и, следовательно, очень быстрыми. Я создаю его при запуске так:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - это контекст, который UI использует везде. Это дочерний элемент masterManagedObjectContext. Я создаю это так:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - этот контекст создается в моем подклассе NSOperation, который отвечает за импорт данных XML в Core Data. Я создаю его в основном методе операции и связываю там с главным контекстом.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

На самом деле это работает очень, ОЧЕНЬ быстро. Просто выполнив эти 3 настройки контекста, я смог увеличить скорость импорта более чем в 10 раз! Честно говоря, в это трудно поверить. (Этот базовый дизайн должен быть частью стандартного шаблона Core Data ...)

В процессе импорта я сохраняю 2 разными способами. Каждые 1000 элементов, которые я сохраняю в фоновом контексте:

BOOL saveSuccess = [backgroundContext save:&error];

Затем в конце процесса импорта я сохраняю основной / родительский контекст, который якобы выталкивает изменения в другие дочерние контексты, включая основной контекст:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

Проблема : проблема в том, что мой пользовательский интерфейс не обновится, пока я не перезагружу представление.

У меня есть простой UIViewController с UITableView, в который загружаются данные с помощью NSFetchedResultsController. Когда процесс импорта завершается, NSFetchedResultsController не видит никаких изменений из родительского / основного контекста, и поэтому пользовательский интерфейс не обновляется автоматически, как я привык видеть. Если я вытащу UIViewController из стека и снова загрузю его, все данные будут там.

Вопрос : Как мне заставить мой дочерний контекст видеть изменения, сохраняющиеся в родительском контексте, чтобы они запускали мой NSFetchedResultsController для обновления пользовательского интерфейса?

Я пробовал следующее, что просто зависает в приложении:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
Дэвид Вайс
источник
26
+1000000 за наиболее сформированный и подготовленный вопрос. У меня тоже есть ответ ... Однако, чтобы напечатать его, потребуется несколько минут ...
Джоди Хагинс,
1
Когда вы говорите, что приложение зависло, где оно? Что он делает?
Джоди Хагинс
Извините, что поднял этот вопрос по прошествии долгого времени. Не могли бы вы прояснить, что означает «Импортировать плоский, а затем исправить отношения в конце»? Разве вам все еще не нужно иметь эти объекты в памяти, чтобы устанавливать отношения? Я пытаюсь реализовать решение, очень похожее на ваше, и мне действительно нужна помощь, чтобы уменьшить объем памяти.
Андреа Спрега
См. Документацию Apple по ссылке в первой части этой статьи. Это объясняет это. Удачи!
Дэвид Вайс
1
Действительно хороший вопрос, и я взял несколько изящных уловок из предоставленного вами описания вашей установки
djskinner

Ответы:

47

Вам, вероятно, следует также постепенно сохранить главный MOC. Нет смысла заставлять этот MOC ждать до конца, чтобы сохранить. У него есть собственный поток, и он также поможет уменьшить объем памяти.

Вы написали:

Затем в конце процесса импорта я сохраняю основной / родительский контекст, который якобы выталкивает изменения в другие дочерние контексты, включая основной контекст:

В вашей конфигурации у вас есть два дочерних элемента (основной MOC и фоновый MOC), оба являются родительскими для «мастера».

Когда вы экономите на дочернем элементе, он передает изменения в родительский. Другие дочерние элементы этого MOC увидят данные в следующий раз, когда они выполнят выборку ... они явно не уведомлены.

Таким образом, при сохранении BG его данные передаются в MASTER. Обратите внимание, однако, что ни одна из этих данных не будет на диске, пока МАСТЕР не сохранит. Более того, любые новые элементы не получат постоянных идентификаторов, пока MASTER не сохранит их на диск.

В вашем сценарии вы загружаете данные в ОСНОВНОЙ MOC путем слияния из МАСТЕР-сохранения во время уведомления DidSave.

Это должно сработать, поэтому мне любопытно, где он «висит». Замечу, что вы не работаете в основном потоке MOC каноническим способом (по крайней мере, не для iOS 5).

Кроме того, вы, вероятно, заинтересованы только в слиянии изменений из главного MOC (хотя ваша регистрация в любом случае выглядит так, как будто она предназначена только для этого). Если бы я использовал уведомление об обновлении при сохранении, я бы сделал это ...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

Теперь, что может быть вашей реальной проблемой относительно зависания ... вы показываете два разных вызова для сохранения на главном устройстве. первый хорошо защищен в своем собственном блоке выполнения, а второй - нет (хотя вы можете вызывать saveMasterContext в блоке выполнения ...

Однако я бы также изменил этот код ...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

Однако обратите внимание, что MAIN является дочерним элементом MASTER. Таким образом, не должно происходить слияния изменений. Вместо этого просто следите за DidSave на мастере и просто обновите! Данные уже находятся в вашем родителе и просто ждут, когда вы их попросите. Это одно из преимуществ наличия данных в родительском элементе.

Еще одна альтернатива для рассмотрения (и мне было бы интересно услышать о ваших результатах - это много данных) ...

Вместо того, чтобы делать фоновый MOC дочерним по отношению к MASTER, сделайте его дочерним по отношению к MAIN.

Получить это. Каждый раз, когда BG сохраняет, он автоматически помещается в MAIN. Теперь MAIN должен вызвать save, а затем мастер должен вызвать save, но все, что они делают, это перемещает указатели ... пока мастер не сохранит на диск.

Прелесть этого метода в том, что данные поступают из фонового MOC прямо в MOC ваших приложений (затем проходят через них для сохранения).

Существует некоторый штраф за передачу, но вся тяжелая работа выполняется в MASTER, когда он ударяется о диск. И если вы удаляете эти сохранения на мастере с помощью performBlock, тогда основной поток просто отправляет запрос и немедленно возвращается.

Пожалуйста, дайте мне знать, как это происходит!

Джоди Хейгинс
источник
Отличный ответ. Я попробую эти идеи сегодня и посмотрю, что открою. Спасибо!
Дэвид Вайс
Потрясающие! Это сработало отлично! Тем не менее, я собираюсь попробовать ваше предложение MASTER -> MAIN -> BG и посмотреть, как это работает, это кажется очень интересной идеей. Спасибо за отличные идеи!
Дэвид Вайс
4
Обновлено, чтобы изменить performBlockAndWait на performBlock. Не уверен, почему это снова появилось в моей очереди, но когда я прочитал это на этот раз, это было очевидно ... не знаю, почему я отказался от этого раньше. Да, performBlockAndWait повторно используется. Однако во вложенной среде, подобной этой, вы не можете вызвать синхронную версию в дочернем контексте из родительского контекста. Уведомление может быть (в данном случае) отправлено из родительского контекста, что может вызвать взаимоблокировку. Я надеюсь, что это понятно любому, кто придет и прочитает это позже. Спасибо, Дэвид.
Джоди Хагинс
1
@DavidWeiss Вы пробовали MASTER -> MAIN -> BG? Я заинтересован в этом шаблоне проектирования и надеюсь узнать, подходит ли он вам. Спасибо.
nonamelive
2
Проблема с шаблоном MASTER -> MAIN -> BG заключается в том, что когда вы извлекаете из контекста BG, он также будет извлекаться из MAIN, что заблокирует пользовательский интерфейс и заставит ваше приложение не реагировать
Ростислав