Использование основных данных одновременно и надежно

Я создаю свое первое приложение для iOS, которое теоретически должно быть довольно простым, но мне трудно сделать его достаточно пуленепробиваемым, чтобы я чувствовал себя уверенно, отправляя его в App Store.

Вкратце, главный экран имеет табличное представление, после выбора строки он переходит к другому табличному представлению, которое отображает информацию, относящуюся к выбранной строке, в режиме мастер-детализации. Базовые данные извлекаются в виде данных JSON из веб-службы один раз в день, а затем кэшируются в хранилище базовых данных. Данные, предшествующие этому дню, удаляются, чтобы предотвратить бесконечный рост файла базы данных SQLite. Все операции с сохранением данных выполняются с использованием Core Data, сNSFetchedResultsController подкрепление подробного табличного представления.

Проблема, с которой я сталкиваюсь, заключается в том, что если вы быстро переключаетесь между главным и подробным экранами несколько раз, когда свежие данные извлекаются, анализируются и сохраняются, приложение полностью зависает или вылетает. Кажется, что существует какое-то состояние гонки, возможно, из-за того, что Core Data импортирует данные в фоновом режиме, пока основной поток пытается выполнить выборку, но я предполагаю. У меня возникли проблемы с получением какой-либо значимой информации о сбое, обычно это SIGSEGV глубоко в стеке Core Data.

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

Main Thread                          Background Thread
viewDidLoad

                                     Get JSON data (using AFNetworking)

Create child NSManagedObjectContext (MOC)

                                     Parse JSON data
                                     Insert managed objects in child MOC
                                     Save child MOC
                                     Post import completion notification

Receive import completion notification
Save parent MOC
Perform fetch and reload table view

                                     Delete old managed objects in child MOC
                                     Save child MOC
                                     Post deletion completion notification

Receive deletion completion notification
Save parent MOC

Как только блок завершения AFNetworking запускается, когда поступают данные JSON,NSManagedObjectContext создается и передается «импортеру»; объект, который анализирует данные JSON и сохраняет объекты в хранилище базовых данных. Импортер выполняет с использованием новогоperformBlock Метод, представленный в iOS 5:

NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setParentContext:self.managedObjectContext];        
    [child performBlock:^{
        // Create importer instance, passing it the child MOC...
    }];

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

Я использую тот же самый базовый шаблон с «удалением» объект для удаления старых данных после импорта новых данных за день. Это происходит асинхронно после того, как новые данные были получены контроллером полученных результатов, и представление таблицы подробностей было перезагружено.

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

Ответы на вопрос(4)

До iOS 5 у нас обычно было дваNSManagedObjectContexts: один для основного потока, один для фонового потока. Фоновый поток может загрузить или удалить данные, а затем сохранить. РезультирующийNSManagedObjectContextDidSaveNotification затем был передан (как вы делаете) в основной поток. Мы звалиmergeChangesFromManagedObjectContextDidSaveNotification: чтобы привести их в основной контекст потока. Это хорошо сработало для нас.

Одним из важных аспектов этого является то, чтоsave: на фоне темыblocks пока послеmergeChangesFromManagedObjectContextDidSaveNotification: завершает работу в главном потоке (потому что мы вызываем mergeChanges ... от слушателя до этого уведомления). Это гарантирует, что контекст управляемого объекта основного потока видит эти изменения. Я не знаю, если выneed сделать это, если у вас есть отношения родитель-ребенок, но вы делали это в старой модели, чтобы избежать различных неприятностей.

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

Какой код вы используете, когда контроллеры появляются / исчезают, что может вызвать проблемы с основными данными? Какие следы стека вы видите при сбое?

 John Topley05 июл. 2012 г., 10:27
Благодарю. Я на самом деле не используюmergeChangesFromManagedObjectContextDidSaveNotification: потому что при обычном использовании новые данные отображаются совершенно нормально без него. Тем не менее, я использовал его в предыдущей сборке, и у меня были те же сбои при быстром переключении между двумя экранами.

Основная проблема, с которой я столкнулся с многопоточными данными ядра, - это непреднамеренный доступ к управляемому объекту в потоке / очереди, отличном от того, в котором он был создан.

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

Это будет включать в себя создание подклассов NSManagedObjectContext и NSManagedObject:

Add a iVar to the MOC subclass and assign to it the queue it was created on. Your MO subclass should check the current queue is the same as its MOC's queue property.

Это всего лишь несколько строк кода, но длительный срок может помешать вам совершать ошибки, которые иначе трудно отследить.

 05 июл. 2012 г., 11:03
Как насчет dispatch_get_current_queue внутри вашего executeBlock:
 John Topley05 июл. 2012 г., 10:36
Спасибо, но я не уверен, как получить очередь MOC, так как она создается с использованиемinitWithConcurrencyType:NSPrivateQueueConcurrencyType Это означает, что он создает собственную частную очередь отправки.

Просто архитектурная идея:

С указанным вами шаблоном обновления данных (один раз в день, полный цикл удаления и добавления данных) я на самом деле буду мотивирован на создание нового постоянного хранилища каждый день (т. Е. С именем для календарной даты), а затем в уведомлении о завершении Табличное представление устанавливает новый контроллер fetchedresults, связанный с новым хранилищем (и, вероятно, новым MOC), и обновляет его. Затем приложение может (в другом месте, возможно, также вызванное этим уведомлением) полностью уничтожить «старый» хранилище данных. Этот метод отделяет обработку обновлений от хранилища данных, которое приложение в настоящее время использует, и переключателя & quot; к новым данным можно считать значительно большеatomicпоскольку изменение происходит, просто начинайте указывать на новые данные, вместо того чтобы надеяться, что вы не перехватываете хранилище в несогласованном состоянии, пока записываются новые данные (но еще не завершены).

Очевидно, я упустил некоторые детали, но я склонен думать, что меняется много данныхwhile being used следует реструктурировать, чтобы уменьшить вероятность того, что вы столкнетесь с аварией.

Рад обсудить дальше ...

 John Topley05 июл. 2012 г., 10:25
Мне нравится эта идея, мне нравитсяa lot, Преимущество также состоит в том, что удаление файловой системой файла SQLite будет происходить намного быстрее, чем удаление базовых данных управляемыми объектами в графе объектов, хотя, конечно, фактически производительность удаления не имеет значения, поскольку другое постоянное хранилище будет использовал в любом случае. Я собираюсь попробовать этот подход в эти выходные.

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

Мой первоначальный вопрос: как повторное извлечение и перезагрузка таблицы относится к началу операции удаления. Есть ли вероятность, что блок удаления сохранит дочерний MOC, покаNSFetchedResultsController все еще загружается или нет?

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

Одна альтернатива, чтобы сделать это более устойчивым, состоит в том, чтобы использовать образец, подобныйUIManagedDocument использует:

Вместо использования родительского MOC в качестве типа параллелизма основного потока,UIManagedDocument фактически создает основной MOC как частную очередь и делает доступным для вас дочерний MOC в основном потоке. Преимущество здесь в том, что все операции ввода-вывода выполняются в фоновом режиме и сохраняются в родительском MOC, вообще не влияя на дочерний MOC, пока дочерний MOC явно не узнает о них. Это потому, что save сохраняет изменения от дочернего к родительскому, а не наоборот.

Таким образом, если вы удалили родительскую очередь, которая является частной, это не приведет кNSFetchedResultsController сфера вообще. А поскольку это старые данные, это на самом деле предпочтительный способ.

Альтернатива, которую я предлагаю, - использовать три контекста:

Main MOC (NSPrivateQueueConcurrencyType)

Responsible for persistent store and deletion of old data.

Child MOC A (NSMainQueueConcurrencyType)

Responsible for anything UI related and NSFetchedResultsController

Child MOC B (NSPrivateQueueConcurrencyTypeДитя ребенка МОС А)

Responsible for inserting new data and committing it up to Child MOC A when done.

Ваш ответ на вопрос