Подтвердить что ты не робот

Внедрение быстрого и эффективного импорта основных данных на iOS 5

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

Вот настройки:

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

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

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

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

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

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

iDeveloper TV - Core Data Performance

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

iDeveloper TV - обновление основных данных для Mac, iPhone и iPad

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

Импорт и отображение больших наборов данных в базовых данных. Автор Marcus Zarra

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

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

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

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

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

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

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

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

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

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

В процессе импорта я сохраняю 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];
}
4b9b3361

Ответ 1

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

Вы писали:

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

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

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

Итак, когда BG сохраняет, его данные переносятся в MASTER. Обратите внимание, однако, что ни один из этих данных не находится на диске, пока не будет сохранен MASTER. Кроме того, любые новые элементы не будут получать постоянные идентификаторы, пока MASTER не сохранит на диск.

В вашем сценарии вы извлекаете данные в MAIN MOC путем слияния с сохранением MASTER во время уведомления DidSave.

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

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

- (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
    }];
}

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

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

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

    // Make sure the master runs in it 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, когда он попадает на диск. И если вы нажмете эти данные на хозяине с помощью функции executeBlock, то основной поток просто отправит запрос и немедленно вернется.

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