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

Фоновый управляемый объект Контекст Ошеломляет анимацию пользовательского интерфейса

У меня есть doozey проблемы, над которой я работал в течение нескольких недель. Это связано с заиканием производительности пользовательского интерфейса, когда я сохраняю контекст управляемого объекта данных. Я добрался, насколько могу, сам и ищу какую-то помощь.

Ситуация

В моем приложении используются два экземпляра NSManagedObjectContext. Один принадлежит делегату приложения и имеет прикрепленный к нему постоянный координатор хранилища. Другой является дочерним элементом основного MOC и относится к объекту Class, называемому PhotoFetcher. Он использует NSPrivateQueueConcurrencyType, поэтому все операции, выполняемые в этом MOC, выполняются в фоновом режиме.

Наше приложение загружает данные JSON, представляющие данные о фотографиях из нашего API. Чтобы получить данные из нашего API, выполняется следующая последовательность шагов:

  • Создайте объект NSURLRequest и используйте протокол NSURLConnectionDataDelegate для построения данных, возвращенных из запроса, или обрабатывайте ошибки.
  • После завершения загрузки данных JSON выполните блок вторичной очереди MOC, который выполняет следующие действия:
    • Разберите JSON с помощью NSJSONSerialization в экземплярах класса Foundation.
    • Итерации по анализируемым данным, вставка или обновление объектов в моем фоновом контексте по мере необходимости. Как правило, это приводит к появлению около 300 новых или обновленных объектов.
    • Сохранить фоновый контекст. Это распространяет мои изменения на основной MOC.
    • Выполните блок на основном MOC, чтобы сохранить его контекст. Это значит, что наши данные сохраняются на диске, a SQLite. Наконец, сделайте обратный вызов делегату, сообщив им, что ответ полностью вставлен в хранилище основных данных.

Код для сохранения фона MOC выглядит примерно так:

[AppDelegate.managedObjectContext performBlock:^{
    [AppDelegate saveContext]; //A standard save: call to the main MOC
}];

Когда сохраняется контекст основного объекта, он также сохраняет большое количество JPEG файлов, которые были загружены с момента последнего сохранения сохранения контекста основного объекта. В настоящее время на iPhone 4 мы загружаем 15 200x200 JPEG с 70% сжатием или всего около 2 МБ данных.

Проблема

Это работает и работает хорошо. Моя проблема заключается в том, что после сохранения фонового контекста NSFetchedResultsController, запущенный в моем контроллере просмотра, получает изменения, распространяемые до основного MOC. Он вставляет новые ячейки в наш PSTCollectionView, клон с открытым исходным кодом UICollectionView. При вставке новых ячеек основной контекст сохраняет и записывает эти изменения на диск. Это может произойти на iPhone 4 под управлением iOS 5.1 в любом месте от 250-350 мс.

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

Я запустил наше приложение в Инструментах с помощью Time Profiler, чтобы определить, что блокирует наш основной поток. К сожалению, результаты были довольно непрозрачными. Это самая тяжелая трассировка стека из инструментов.

Instruments Heaviest Stack Trace

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

Трассировка в текстовой форме выглядит следующим образом:

Symbol Name
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
 -[NSManagedObjectContext executeFetchRequest:error:]
  -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]
   _perform
    _dispatch_barrier_sync_f_invoke
     _dispatch_client_callout
      __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0
       -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
        -[NSManagedObjectContext executeFetchRequest:error:]
         -[NSPersistentStoreCoordinator executeRequest:withContext:error:]
          -[NSSQLCore executeRequest:withContext:error:]
           -[NSSQLCore objectsForFetchRequest:inContext:]
            -[NSSQLCore newRowsForFetchPlan:]
             -[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:]
              -[NSSQLiteConnection execute]

Что я пробовал

Прежде чем мы коснулись кода Core Data, первое, что мы сделали, это оптимизировать наши JPEG. Мы переключились на меньшие JPEG и увидели повышение производительности. Затем мы уменьшили количество загружаемых файлов JPEG (от 90 до 15). Это также приводит к значительному повышению производительности. Тем не менее, мы по-прежнему видим блоки длиной 250-350 мс в основном потоке.

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

Изменение постоянного хранилища до NSInMemoryStoreType не имело эффекта.

Может ли кто-нибудь указать мне на "секретный соус", который даст мне представление пользовательского интерфейса, которое обещали фоновые контексты объектов?

4b9b3361

Ответ 1

У меня есть несколько догадок в том порядке, в котором вы должны их проверить:

  • Похоже, что все, что происходит медленно, происходит после сохранения данных (т.е. в обратном вызове). Это приводит меня к тому, что это перезагрузка PTSCollectionView или повторная выборка получаемого контроллера результатов.

  • Возникает ли эта проблема с UICollectionView на iOS 6.x? Если нет, это приведет меня к PTSCollectionView.

  • Если это все еще происходит, значит, это скорее не представление коллекции, а вместо получателя результатов. Как бы это выглядело, из фреймов стека (непрозрачных, хотя они могут быть), что выбранный контроллер результатов пытается выполнить выборку, которая происходит через dispatch_barrier. Они используются для обеспечения того, чтобы блок не выполнялся до тех пор, пока не будет достигнут барьер. Я здесь на конечности, но вы можете проверить это, потому что внутренне Core Data сохраняет в другом месте и, таким образом, задерживает выполнение любых других запросов на выборку. Опять же, такая дикая и необразованная догадка. Но я бы попробовал без исправления обновленного контроллера результатов сразу же и посмотрел, не произошло ли еще заикание.

Еще одна вещь, которая выделяется для меня: вы выполняете большую работу над дочерним MOC, но затем выполняете сохранение родителя. Похоже, что основная часть сохранения должна выполняться ребенком. Но также может быть, я не работал с этой частью Core Data через некоторое время: -)

Ответ 2

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

Во-первых, я предполагаю, что ваш основной MOC был создан с помощью

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

Я делаю это предположение, потому что...

  • Вы используете его как родительский контекст, поэтому он должен быть либо NSMainQueueConcurrencyType, либо NSPrivateQueueConcurrencyType.

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

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

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

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

Итак, то, что вы видите, полностью ожидается.

Есть много вариантов решения вашей проблемы.

Мое первое предложение состояло в том, чтобы создать MOC с частной очереди и сделать его родительским MOC главной очереди.

Это означает, что любое сохранение MOC главной очереди будет не. Вместо этого он "сохранит" данные родительскому элементу, который затем выполнит фактическое сохранение базы данных в собственной частной очереди и, как результат, в отдельном потоке в фоновом режиме.

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

Обратите внимание, что в iOS 5 есть несколько ошибок, связанных с вложенными контекстами, но если вы ориентируетесь на iOS 6, большинство из них были исправлены.

См. Основные данные не могут выполнить полную проверку для объекта после полученияPermanantIDs для получения дополнительной информации.

ИЗМЕНИТЬ

Ваши предположения верны, хотя я боюсь, что я нацелен на iOS 5 и может иметь только родительский MOC для основного MOC на iOS 6 (он вызывает тупик с FRC). - Пепельная ярость

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

Если вы не можете этого сделать, есть еще несколько вариантов. Тот, который мне больше всего нравится, - это присоединение FRC к приватной очереди-MOC, а затем "склеить" ваш доступ к FRC через главный поток. Вы можете dispatch_async в основной поток обрабатывать обновления делегата в виде таблицы (или что-то еще). Например...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    dispatch_async(dispatch_get_main_queue(), ^{
        [tableView beginUpdates];
    });
}

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

На самом деле это проще, чем кажется.

По сути, у вас будет такая же настройка, как у вас сейчас, за исключением того, что "main-MOC" будет MOC частной очереди вместо MOC главной очереди. Таким образом, ваш основной поток не будет блокироваться при доступе к базе данных.

Однако вам нужно принять несколько мер предосторожности, чтобы убедиться, что вы используете FRC в соответствующем контексте.

Другим вариантом является использование вашего основного MOC и FRC, как вы делаете сейчас, для обработки изменений в базе данных, но вносите все изменения в базу данных через отдельный MOC, прикрепленный непосредственно к координатору постоянного хранилища. Это приводит к изменениям в отдельном потоке. Затем вы используете уведомления о сохранении MOC для обновления вашего контекста и/или получения данных из хранилища.

Обновления iOS и OSX устраняют множество проблем с вложенными контекстами для Core Data, но есть некоторые вещи, о которых вам нужно беспокоиться при поддержке предыдущей версии.