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

Ловушки использования двух постоянных координаторов хранилища для эффективного обновления фона

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

В этой теме есть хорошие материалы, в том числе:

Основываясь на моих исследованиях и личном опыте, лучшим вариантом является эффективное использование двух отдельных стеков данных ядра, которые используют только данные на уровне базы данных (SQLite). Это означает, что нам нужны два отдельных NSPersistentStoreCoordinators, каждый из которых имеет свой собственный NSManagedObjectContext. Когда запись в режиме записи включена в базе данных (по умолчанию от iOS 7 и далее), необходимость блокировки можно было бы избежать почти во всех случаях (кроме случаев, когда у нас есть две или более одновременных записей, что маловероятно в моем сценарии).

Для эффективного обновления фона и сохранения памяти необходимо также обрабатывать данные партиями и периодически сохранять фоновый контекст, поэтому грязные объекты сохраняются в базе данных и очищаются от памяти. Можно использовать NSManagedObjectContextDidSaveNotification, который генерируется в этот момент, чтобы объединить изменения фона в основной контекст, но в целом вы не хотите обновлять свой интерфейс сразу после сохранения партии. Вы хотите подождать, пока фоновая работа будет полностью выполнена, и обновите пользовательский интерфейс (рекомендуется как в сессии WWDC, так и в статьях objc.io). Это фактически означает, что основной контекст приложения остается не синхронизированным с базой данных в течение определенного периода времени.

Все это приводит меня к моему основному вопросу: , что может пойти не так, если я изменил базу данных таким образом, не указав основной контекст на слияние изменений? Я предполагаю это не все солнце, розы.

Один конкретный сценарий, который у меня есть в голове, что происходит, если ошибка должна выполняться для объекта, загруженного в основной контекст, если фоновая операция между удаленным этим объектом из базы данных? Может ли это, например, произойти в представлении таблицы NSFetchedResultsController, которое использует пакетный размер для выборочной выборки объектов в память? I.e., объект, который еще не был полностью извлечен, удаляется, но мы прокручиваем его до точки, где объект должен загружаться. Это потенциальная проблема? Могут ли другие вещи пойти не так? Буду признателен за любые материалы по этому вопросу.

4b9b3361

Ответ 1

Отличный вопрос!

I.e. объект, который еще не был полностью извлечен, удаляется, но чем мы прокручиваем до точки, где объект должен загружаться. Является это потенциальная проблема?

К сожалению, это вызовет проблемы. Вызывается следующее исключение:

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xc544570 <x-coredata://(...)>'

Это сообщение в блоге (раздел "Как сделать concurrency с основными данными?" ) может быть несколько полезным, но он не исчерпывает эту тему. Я борюсь с теми же проблемами в приложении, над которым я сейчас работаю, и хотел бы прочитать рецензию об этом.

Ответ 2

Основываясь на вашем вопросе, комментариях и моем собственном опыте, кажется, что большая проблема, которую вы пытаетесь решить: 1. Использование NSFetchedResultsController в основном потоке с ограничением потока 2. Импорт большого набора данных, который будет вставлять, обновлять или удалять управляемые объекты в контексте. 3. Импорт вызывает большие уведомления о слиянии, которые должны обрабатываться основным потоком для обновления пользовательского интерфейса. 4. Большое слияние имеет несколько возможных эффектов:  - Пользовательский интерфейс работает медленно или слишком занят, чтобы его можно было использовать. Возможно, это связано с тем, что вы используете beginUpdates/endUpdates обновить табличное представление в NSFetchedResultsControllerDelegate, и у вас много анимаций, связанных с большим слиянием.  - Пользователи могут запускать "Не удалось выполнить ошибку", поскольку они пытаются получить доступ к поврежденному объекту, который был удален из хранилища. Контекст управляемых объектов считает, что он существует, но когда он переходит в хранилище, чтобы выполнить ошибку, он уже был удален. Если вы используете reloadData, чтобы обновить табличное представление в вашем NSFetchedResultsControllerDelegate, вы, скорее всего, увидите это, чем при использовании beginUpdates/endUpdates.

Подход, который вы пытаетесь использовать для решения вышеуказанных проблем: - Создайте два NSPsistentStoreCoordinators, каждый из которых прикреплен к одному и тому же NSPsistentStore или по крайней мере одному и тому же адресу файла хранилища SQLite NSPersistentStore. - Ваш импорт происходит в NSManagedObjectContext 1, подключенном к NSPersistentStoreCoordinator 1, и выполняется на некоторых других потоках. Ваш NSFetchedResultsController использует NSManagedObjectContext 2, прикрепленный к NSPersistentStoreCoordinator 2, работающий в основном потоке. - Вы перемещаете изменения из NSManagedObjectContext 1 в 2

У вас возникнут некоторые проблемы с этим подходом. - - это посредничество между подключенными NSManagedObjectContexts и он прилагается магазины. В описываемом сценарии множественного координатора-контекста изменения в базовом хранилище NSManagedObjectContext 1, которые вызывают изменение в файле SQLite, не будут рассматриваться NSPersistentStoreCoordinator 2 и его контекстом. 2 не знает, что 1 изменил файл, и у вас будет "Не удалось выполнить ошибку" и другие интересные исключения. - В какой-то момент вы все равно должны поместить измененные NSManagedObjects из импорта в NSManagedObjectContext 2. Если эти изменения будут большими, у вас все еще будут проблемы с пользовательским интерфейсом, и пользовательский интерфейс будет не синхронизирован с хранилищем, что потенциально приведет к "Не удалось выполнить ошибку". - В общем, поскольку NSManagedObjectContext 2 не использует тот же NSPersistentStoreCoordinator, что и NSManagedObjectContext 1, у вас возникнут проблемы с отсутствием синхронизации. Это не значит, что эти вещи предназначены для совместного использования. Если вы импортируете и сохраняете в NSManagedObjectContext 1, NSManagedObjectContext 2 немедленно находится в состоянии, не соответствующем хранилищу.

Это НЕКОТОРЫЕ вещи, которые могут пойти не так с этим подходом. Большинство из этих проблем станут видны при запуске ошибки, потому что он обращается к магазину. Вы можете узнать больше о том, как этот процесс работает в Руководстве по программированию основных данных, а Руководство по программированию инкрементального магазина описывает процесс более подробно. Хранилище SQLite следует тому же процессу, что и реализация инкрементного хранилища.

Опять же, пример использования, который вы описываете, - получение тонны новых данных, выполнение find-Or-Create данных для создания или обновлять управляемые объекты и удалять "устаревшие" объекты, которые на самом деле могут быть большей частью магазина, - это то, что я рассматривал каждый день в течение нескольких лет, видя все те же проблемы, что и вы. Существуют решения - даже для импорта, которые одновременно меняют 60 000 сложных объектов и даже используют ограничение потока! - но это выходит за рамки вашего вопроса. (Подсказка: контексты родителя и ребенка не нуждаются в уведомлениях о слиянии).

Ответ 3

Два постоянных координатора хранилища (pscs) - это, безусловно, путь к большим наборам данных. Блокировка файлов происходит быстрее, чем блокировка данных ядра.

Нет причин, по которым вы не могли бы использовать фоновый psc для создания потоков, ограниченных NSManagedObjectContexts, в которых каждый создается для каждой операции, выполняемой в фоновом режиме. Однако вместо того, чтобы позволить основным данным управлять очередью, вам теперь нужно создавать NSOperationQueues и/или потоки для управления операциями на основе того, что вы делаете в фоновом режиме. NSManagedObjectContexts являются бесплатными и не дорогостоящими. Как только вы это сделаете, вы можете висеть на свой NSManagedObjectContext и использовать его только в течение одной операции и/или времени жизни нитей и строить столько изменений, сколько хотите, и ждать до конца, чтобы зафиксировать их и объединить их в основной поток, как бы вы ни были принимать решение. Даже если у вас есть записи основного потока, вы все равно можете в критических точках вашего жизненного цикла выполнять повторный сбор/слияние обратно в контекст ваших потоков.

Также важно знать, что если вы работаете над большими наборами данных, не беспокойтесь о слиянии контекстов, пока вы не трогаете что-то еще. Например, если у вас есть класс A и класс B, и у вас есть две отдельные операции/потоки, чтобы работать с ними, и у них нет прямых отношений, вам не нужно объединять контексты, если они меняются, вы можете продолжать катиться с изменениями. Единственная важная потребность в объединении фоновых контекстов таким образом - это если есть прямые связи. Было бы лучше предотвратить это, хотя через какую-то сериализацию, будь то NSOperationQueue или что-то еще. Поэтому не стесняйтесь работать на разных объектах в фоновом режиме, просто будьте осторожны с их отношениями.

Я работал над крупномасштабными базовыми проектами данных, и эта модель очень хорошо работала для меня.

Ответ 4

Действительно, это лучший сценарий основных данных, с которым вы можете работать. Практически нет основной привязки к пользовательскому интерфейсу и простое управление данными. Если вы хотите сообщить главный контекст (и, возможно, текущий NSFetchedResultsController), вы прослушиваете уведомления о сохранении в backgroundContext следующим образом:

    [[NSNotificationCenter defaultCenter] 
      addObserver:self selector:@selector(reloadFetchedResults:)
      name:NSManagedObjectContextDidSaveNotification
      object:backgroundObjectContext];

Затем вы можете объединить изменения, но ожидая, что контекст Main Thread поймает их перед сохранением. Когда вы получаете уведомление mergeChangesFromContextDidSaveNotification, изменения еще не сохранены. Следовательно, performBlockAndWait является обязательным, поэтому основной контекст получает изменения, а затем NSFetchedResultsController корректно обновляет свои значения.

-(void)reloadFetchedResults:(NSNotification*)notification
{
    NSManagedObjectContext*moc=[notification object];
    if ([moc isEqual:backgroundObjectContext]) 
    {
        // Delete caches of fethcedResults if you have a deletion
        if ([[theNotification.userInfo objectForKey:NSDeletedObjectsKey] count]) {
            [NSFetchedResultsController deleteCacheWithName:nil];
         }
        // Block the background execution of the save, and merge changes before
        [managedObjectContext performBlockandWait:^{
            [managedObjectContext 
            mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Есть ложь, которую никто не заметил. Вы можете получить уведомление о сохранении до того, как фоновый контекст фактически сохранил объект, который вы хотите объединить. Если вы хотите избежать проблем с помощью более быстрого Main Context, запрашивающего объект, который еще не был сохранен в фоновом контексте, вы должны (вы действительно) вызывать obtainPermanentIDsForObjects до любой фон сохранить. Тогда вы можете позвонить mergeChangesFromContextDidSaveNotification. Это гарантирует, что слияние получает действительный постоянный идентификатор для слияния.