Раздел основных данныхNameKeyPath с проблемой производительности атрибута отношения

У меня есть базовая модель данных с тремя объектами:
Person, Group, Photo со следующими отношениями между ними:

  • Человек ‹‹-----------> Группа (отношение один ко многим)
  • Человек ‹-------------> Фото (один к одному)

Когда я выполняю выборку, используя NSFetchedResultsController в UITableView, я хочу сгруппировать в разделы объекты Person, используя атрибут name сущности Group.

Для этого я использую sectionNameKeyPath:@"group.name".

Проблема в том, что когда я использую атрибут из отношения Group, NSFetchedResultsController извлекает все заранее небольшими партиями по 20 (у меня setFetchBatchSize: 20), а не извлекает партии, пока я прокручиваю tableView.

Если я использую атрибут объекта Person (например, sectionNameKeyPath:@"name") для создания разделов, все работает нормально: NSFetchResultsController загружает небольшие пакеты из 20 объектов при прокрутке.

Код, который я использую для создания экземпляра NSFetchedResultsController:

- (NSFetchedResultsController *)fetchedResultsController {

    if (_fetchedResultsController) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:[Person description]
                                              inManagedObjectContext:self.managedObjectContext];

    [fetchRequest setEntity:entity];

    // Specify how the fetched objects should be sorted
    NSSortDescriptor *groupSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"group.name"
                                                                        ascending:YES];

    NSSortDescriptor *personSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthName"
                                                                         ascending:YES
                                                                          selector:@selector(localizedStandardCompare:)];


    [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:groupSortDescriptor, personSortDescriptor, nil]];

    [fetchRequest setRelationshipKeyPathsForPrefetching:@[@"group", @"photo"]];
    [fetchRequest setFetchBatchSize:20];

    NSError *error = nil;
    NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];

    if (fetchedObjects == nil) {
        NSLog(@"Error Fetching: %@", error);
    }

    _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                    managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"group.name" cacheName:@"masterCache"];

    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;
}

Вот что я получаю в инструментах, если создаю разделы на основе "group.name" без какого-либо взаимодействия с пользовательским интерфейсом приложения: Извлечение основных данных с  Разделы по взаимосвязи

И это то, что я получаю (с небольшой прокруткой в ​​UITableView), если sectionNameKeyPath равен нулю: Извлечение основных данных без каких-либо разделов

Пожалуйста, может ли кто-нибудь помочь мне в этом вопросе?

ИЗМЕНИТЬ 1:

Кажется, что я получаю противоречивые результаты от симулятора и инструментов: когда я задал этот вопрос, приложение запускалось в симуляторе примерно через 10 секунд (по Time Profiler), используя приведенный выше код.

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

Я приложил несколько свежих скриншотов: Time Profiler with SimulatorПредварительная загрузка в симуляторе без прокрутки Предварительная загрузка в симуляторе с прокруткой  и выборки небольшими партиями

РЕДАКТИРОВАНИЕ 2: я перезагрузил симулятор, и результаты оказались интригующими: после выполнения операции импорта и выхода из приложения первый запуск выглядел так: Первый запуск после сброса симулятора и нового импорта После небольшой прокрутки:   Первый запуск после сброса симулятора, нового импорта и некоторой прокрутки Вот что происходит при втором запуске: Второй запуск после  сброс симулятора и новый импорт После пятого запуска: Пятый запуск

РЕДАКТИРОВАТЬ 3: Запустив приложение в седьмой и восьмой раз, я получаю следующее: Seventh runВосьмой прогон


person Razvan    schedule 27.08.2014    source источник
comment
Я полагаю, что один Рик предложил эту ссылку, но его ответ прошел модерацию. В любом случае... Попробуй. Он думал, что это может ответить на ваш вопрос.   -  person staticVoidMan    schedule 31.08.2014
comment
Хорошо написанный вопрос.   -  person Lorenzo B    schedule 14.09.2014
comment
@codeFi, вас больше всего беспокоит то, что извлечение блокирует взаимодействие с пользователем?   -  person quellish    schedule 15.09.2014
comment
@quellish Да, это блокирует взаимодействие с пользователем при запуске приложения, потому что для представления пользовательского интерфейса требуется много времени, но эта проблема возникает только в симуляторе. Как ни странно, при запуске приложения на iPhone 4S, даже если я предварительно выбираю объекты Group и Photo и использую атрибут name объекта Group как sectionNameKeyPath, приложение загружается примерно через 900 мс.   -  person Razvan    schedule 15.09.2014
comment
Вот и не знаю что с этим делать... в симуляторе получается одно, на девайсе другое...   -  person Razvan    schedule 15.09.2014
comment
Если вы профилируете его по времени, большую часть этого времени занимает выборка или что-то еще? Нередко добавление магазина в постоянный координатор хранилища занимает много времени.   -  person quellish    schedule 15.09.2014
comment
@quellish Я отредактировал свой пост, добавив новую информацию.   -  person Razvan    schedule 15.09.2014
comment
В профиле времени инвертируйте дерево вызовов, не разделяйте по потокам и показывайте самые популярные вызовы, чтобы было очевидно, на что тратится время.   -  person quellish    schedule 15.09.2014
comment
@quellish нет смысла делать это прямо сейчас, потому что кажется, что все загружается очень быстро (690 мс). Но если вы хотите знать, что сейчас занимает больше всего вычислительного времени, так это __pread из libsystem_kernel.dylib (75 мс).   -  person Razvan    schedule 15.09.2014
comment
@codeFi, в комментарии вы говорите, что кажется, что Core Data предпочитает хранить превью как двоичный файл в таблицах базы данных, это на симуляторе или на устройстве? Поведение и производительность внешнего хранилища записей могут существенно различаться между симулятором и устройством.   -  person quellish    schedule 16.09.2014
comment
@quellish это происходит в симуляторе. Я не смотрел на то, что происходит на устройстве с этой точки зрения.   -  person Razvan    schedule 16.09.2014
comment
Когда вы указываете в модели, что Core Data может использовать внешнее хранилище для смоделированного двоичного атрибута, Core Data решает во время выполнения, хранить ли эти двоичные данные в хранилище SQLite или во внешнем файле. Рассуждения Core Data об этом могут отличаться при работе на устройстве и в симуляторе. Кроме того, перенос хранилища, в котором записаны файлы внешних записей, может быть очень медленным. Есть ли основания полагать, что вы выполняли миграцию на запусках инструментов, где вы заметили медлительность?   -  person quellish    schedule 16.09.2014
comment
Нет, я ничего не импортировал. Я проводил тесты с уже заполненной базой данных.   -  person Razvan    schedule 16.09.2014


Ответы (3)


Это ваша заявленная цель: мне нужно, чтобы объекты Person были сгруппированы в разделы по группе объекта отношения, атрибуту имени и NSFetchResultsController для выполнения выборки небольшими партиями при прокрутке, а не заранее, как это делается сейчас.

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

TL;ДР; Чтобы изменить это поведение, вам нужно изменить способ построения секций NSFetchedResultsController.

Что случилось?

Когда NSFetchedResultsController получает запрос на выборку с разбиением на страницы (fetchLimit и/или fetchBatchSize), происходит несколько вещей.

Если sectionNameKeyPath не указано, он делает именно то, что вы ожидаете. Выборка возвращает прокси-массив результатов с реальными объектами для первого количества элементов fetchBathSize. Так, например, если у вас есть setFetchBatchSize на 2, и ваш предикат соответствует 10 элементам в магазине, результаты содержат первые два объекта. Другие объекты будут извлекаться отдельно по мере доступа к ним. Это обеспечивает плавный ответ с разбивкой на страницы.

Однако, когда указано sectionNameKeyPath, контроллер извлеченных результатов должен сделать немного больше. Чтобы вычислить разделы, ему необходимо получить доступ к этому ключевому пути ко всем объектам в результатах. Он перечисляет 10 элементов результатов в нашем примере. Первые два уже получены. Остальные 8 будут выбраны во время перечисления, чтобы получить значение пути ключа, необходимое для построения информации о разделе. Если у вас много результатов для вашего запроса на выборку, это может быть очень неэффективно. Существует ряд общедоступных ошибок, связанных с этой функциональностью:

Контроллеру NSFetchedResultsController изначально требуется слишком много времени для настройки разделов

NSFetchedResultsController игнорирует свойство fetchLimit

Проблема производительности NSFetchedResultsController, индекса таблицы и пакетной выборки

... И ряд других. Когда вы думаете об этом, это имеет смысл. Для создания объектов NSFetchedResultsSectionInfo требуется, чтобы полученный контроллер результатов видел каждое значение в результатах для sectionNameKeyPath, объединял их в уникальный союз значений и использовал эту информацию для создания правильного количества объектов NSFetchedResultsSectionInfo, задавал имя и заголовок индекса, знать, сколько объектов в результатах содержит раздел и т. д. Для обработки общего варианта использования этого нет. Имея это в виду, ваши трассировки инструментов могут иметь гораздо больше смысла.

Как вы можете это изменить?

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

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

Изменение вашей модели данных не изменит описанное выше поведение, но может немного изменить влияние на производительность. Пакетные обновления не окажут существенного влияния на это поведение и фактически не будут хорошо работать с извлеченным контроллером результатов. Однако для вас может быть гораздо полезнее вместо этого установить relationshipKeyPathsForPrefetching для включения ваших групповых отношений, что может значительно улучшить поведение выборки и ошибки. Другая стратегия может состоять в том, чтобы выполнить еще одну выборку для пакетной проверки этих объектов, прежде чем вы попытаетесь использовать полученный контроллер результатов, который будет заполнять различные уровни кэшей Core Data в памяти более эффективным образом.

Кэш NSFetchedResultsController в первую очередь предназначен для информации о разделе. Это предотвращает необходимость полного пересчета разделов при каждом изменении (в лучшем случае), но на самом деле может сделать первоначальную выборку для построения разделов намного дольше. Вам придется поэкспериментировать, чтобы увидеть, подходит ли кеш для вашего варианта использования.

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

person quellish    schedule 15.09.2014
comment
Вы упустили суть моего ответа. Пакетные обновления используются для обновления нового атрибута (скажем, groupName) объекта Person при изменении name объекта Group. И это никак не связано с NSFethedResultsController. О том, что вы здесь говорите: Изменение вашей модели данных не изменит приведенное выше поведение, но может немного изменить влияние на производительность. Я предполагаю, что это неправильно, но для уверенности я приведу пример возврата с некоторыми тестами. - person Lorenzo B; 15.09.2014
comment
Вопрос автора касается NSFetchedResultsController производительности при расчете сечений в первый раз. Пакетные обновления не имеют отношения к этому, и в обычных сценариях будут вредными — координация пакетных изменений с помощью NSFetchedResultsController нетривиальна. Поведение, которое описывает автор и ищет рекомендации, характерно для того, как NSFetchedResultsController должен выполнять свою работу. Нормализация модели данных не меняет этого, это только означает, что потребуется немного больше данных, чтобы увидеть такое же влияние на производительность - в лучшем случае. - person quellish; 15.09.2014
comment
@quellish на самом деле flexaddicted прав: несколько дней назад я пытался использовать атрибут groupName в сущности Person и создавать из него разделы. Это работало, как и ожидалось: больше не было предварительных выборок, объекты Person были сгруппированы по groupName, а FetchController извлекал небольшие пакеты, когда я прокручивал. Однако, с моей точки зрения, такой подход противоречит цели отношений. - person Razvan; 15.09.2014

Основываясь на моем опыте, способ достижения вашей цели — денормализация вашей модели. В частности, вы можете добавить атрибут group в свою сущность Person и использовать этот атрибут как sectionNameKeyPath. Итак, когда вы создаете Person, вы также должны передать группу, к которой он принадлежит.

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

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

Изменить 1

Я не могу понять ваш комментарий, не могли бы вы объяснить лучше?

Что я нашел очень интригующим, так это то, что даже если приложение выполняет полную предварительную выборку в симуляторе, приложение загружается на устройстве за 900 мс (с 5000 объектами), несмотря на симулятор, где оно загружается намного медленнее.

В любом случае, мне было бы интересно узнать подробности о вашей Photo сущности. Если вы предварительно загрузите фотографию, это может повлиять на общее исполнение.

Вам нужно предварительно получить Photo в табличном представлении? Это большие пальцы (маленькие фото)? Или нормальные картинки? Используете ли вы преимущества флажка внешнего хранилища?

Добавление дополнительного атрибута (скажем, group) к объекту Person не может быть проблемой. Обновление значения этого атрибута при изменении name объекта Group не представляет проблемы, если вы выполняете это в фоновом режиме. Кроме того, начиная с iOS 8 доступно пакетное обновление, как описано в разделе Пакетные обновления основных данных.

person Lorenzo B    schedule 14.09.2014
comment
Спасибо за ваш полезный ответ. Я думал об этом, но, как вы сказали, проблема возникает, когда я хочу изменить имя группы для тысяч объектов, связанных с определенной группой. Что я нашел очень интригующим, так это то, что даже если приложение выполняет полную предварительную выборку в симуляторе, приложение загружается за 900 мс (с 5000 объектов) на устройстве, несмотря на симулятор, где оно загружается намного медленнее. Я думаю, что на устройстве происходит что-то еще... Устройство представляет собой iPhone 4S с последней сборкой iOS 7. - person Razvan; 14.09.2014
comment
По поводу последней фразы вы абсолютно правы! Я не буду представлять пользователю тысячи объектов, сгруппированных по разделам в одном табличном представлении. Вместо этого у меня будет табличное представление с некоторыми ячейками, представляющими группы и переходы от каждой из них к содержимому каждой группы. Но это личное упражнение для меня, чтобы понять Core Data, и эта проблема меня очень расстраивает. - person Razvan; 15.09.2014
comment
Добавлены некоторые другие подсказки. - person Lorenzo B; 15.09.2014
comment
Для минусующего. Комментарии к минусам также должны быть вставлены. - person Lorenzo B; 15.09.2014
comment
Заявленная автором цель: мне нужно, чтобы объекты Person были сгруппированы в разделы по группе сущности отношения, атрибуту имени и NSFetchResultsController для выполнения выборки небольшими партиями при прокрутке, а не заранее, как это делается сейчас. Ваш ответ не касается этого. - person quellish; 15.09.2014
comment
@quellish дерномализация модели может улучшить производительность. Если вы поместите новый атрибут, скажем, groupName, в сущность Person, этот атрибут можно будет использовать для группировки и предотвращения предварительных выборок. - person Lorenzo B; 15.09.2014
comment
@flexaddicted денормализация модели не изменит поведение выборки при построении начальных разделов, о чем автор просит совета. Денормализация модели в лучшем случае сократит время на то, что он уже не хочет делать. Если вы достаточно ДЕМОРАЛИЗОВАЛИ полученный контроллер результатов, возможно, это изменило бы его поведение. Но, вероятно, нет. - person quellish; 15.09.2014
comment
@flexaddicted на устройстве приложение загружается очень быстро (за 900 мс), а в симуляторе загружается за несколько секунд. - person Razvan; 15.09.2014
comment
@flexaddicted относительно фотографий, которые я использую для отображения в табличном представлении: это небольшие миниатюры (5 000 по 48 КБ каждая), и для атрибута включена опция «Разрешить внешнее хранилище». Однако кажется, что Core Data предпочитает хранить превью в виде двоичного файла в таблицах базы данных, потому что размер файла sqlite достигает ~ 130 МБ. - person Razvan; 15.09.2014

Спустя почти год с тех пор, как я опубликовал этот вопрос, я наконец нашел виновников, которые позволяют это поведение (которое немного изменилось в Xcode 6):

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

  2. Что касается того факта, что все было получено заранее небольшими партиями без прокрутки (в основных инструментах данных Xcode 6 это уже не так - теперь это одна большая выборка, которая занимает целые секунды):

Кажется, что setFetchBatchSize некорректно работает с parent/child contexts. О проблеме сообщалось еще в 2012 году, и похоже, что она все еще существует http://openradar.appspot.com/11235622< /а>.

Чтобы преодолеть эту проблему, я создал еще один independent context с NSMainQueueConcurrencyType и установил его persistence coordinator таким же, как мои другие contexts.

Подробнее о проблеме № 2 здесь: https://stackoverflow.com/a/11470560/1641848

person Razvan    schedule 28.07.2015