Компания
1 090,96
рейтинг
14 октября 2014 в 14:11

Разработка → Не нужно бояться Core Data

Давно подметил, что среди многих своих коллег по цеху присутствует некоторая подозрительность и даже в некотором роде неприязнь к Core Data, причем некоторые к фреймворку даже и не притрагивались. Чего уж там, и я в начале своего пути освоения новой платформы относился к нему предвзято, пойдя на поводу у подобных комментариев. Но не стоит поддаваться предрассудкам и мифам, не потрогав продукт самостоятельно. Тем из нас, кто пошел «против системы», но еще не постиг инструмент полностью, я и посвящаю эту статью. На основе небольшого примера, основанного на реальной задаче разработки мобильного клиента нашей социальной сети Мой Мир, я хочу рассказать о некоторых «подводных» камнях и заострить внимание начинающего разработчика на важных моментах оптимизации использования Core Data. Предполагается, что читающий уже имеет представление, для чего нужны основные элементы Core Data (NSManagedObjectContext, NSPersistentStoreCoordinator и т.д.) и хотя бы поверхностно ознакомлен с API.

Наш кейс: необходимо разработать приложение, позволяющее хранить и структурировать большой объем фотографий с различной метаинформацией о них. Для этого нам потребуется Core Data… и все.

Core Data rulezzz!

Core Data Stack

Итак, первое, что мы должны сделать — приготовить правильный Core Data стэк. К счастью для нас, есть универсальное решение, я думаю известный всем Best Practice от WWDC 2013:

Core Data Stack

Стэк разделяется на два контекста, Main Context используется на главном потоке для чтения данных; Background Context — для редактирования, вставки и удаления больших объемов данных. То есть, рекомендуется изначально строить архитектуру своего приложения так, что все изменения происходят в бэкграунд контексте, а на главном контексте вы выполняете лишь read-only операции.

Хочется отметить, что по архитектуре стэков написано немало статей, описывающих всевозможные ветвления контекстов. На мой взгляд, они лишь задирают порог вхождения использования Core Data и только отпугивают начинающих разработчиков от применения фреймворка. На деле для 90% приложений будет достаточно вышеприведенной модели, еще 9% будет достаточно вообще одного Main Context и только оставшимся изваращ хардкорщикам нужно что-то более сложное.

Тонкие моменты

  • Начиная с iOS 7 sqlite хранилище в отличие от предыдущих версий работает в режиме WAL(Write Ahead Log) журналирования, который позволяет совершать одну операцию записи и множественные операции чтения параллельно. Если вдруг вы поддерживаете iOS 6, то возможно включить этот режим при создании координатора стэка в версиях iOS 4+ с помощью NSSQLitePragmasOption, однако это может быть чревато неприятностями. Также в iOS 6 в стэке с двумя координаторами при синхронизации контекстов через нотификацию могут не обновляться объекты в них. Поэтому для iOS 6 лучше использовать стек с двумя контекстами, имеющими общий координатор и не заморачиваться с режимом журналирования, процент активных устройств крайне низок.
  • WAL также хранит в себе мину замедленного действия в виде поломанной ручной миграции и возможных ошибок бэкапирования. Так как хранилище на диске организовано в виде трех файлов: dbname.sqlite, dbname.sqlite-wal и dbname.sqlite-shm, то при организации ручного бэкапа нужно не забывать сохранять их всех, иначе потом вас будет ждать очень «приятный» сюрприз. Инженеры Apple видимо сами забыли о наличии WAL файла, поэтому при использовании Migration Manager мы также можем разломать базу. Сам я не сталкивался с подобной проблемой, почитать подробнее можно здесь.
  • Типичные мануалы по Core Data и шаблон проекта в Xcode предлагают размещать стэк прямо в классе AppDelegate и инициализировать всё необходимое во время запуска приложения. Однако если в вашем приложении работа с базой имеет эпизодический или опциональный характер (например, она нужна только после регистрации пользователя в приложении и не нужна при гостевом доступе), имеет смысл вынести стек «в бок». Для этого подойдет отдельный Singleton класс, который будет инициализироваться непосредственно в тот момент, когда это действительно нужно. Это позволит сохранить существенный объем памяти и сократить время запуска приложения.

Проектирование модели

Продумывание схемы данных — самый важный момент при работе с Core Data. Исправление ошибки, допущенной на этапе проектирования архитектуры, может стоить разработчику кучу времени и нервов. Идеальный вариант, если после выхода в бой модель не изменяется. В реальности, если вам не придется прибегать к ручной миграции через Migration Manager и все изменения проглатываются Lightweight Migration — вы молодец. Уделяйте этому этапу как можно больше времени и старайтесь экспериментировать с разными вариантами моделей.

Вернемся к нашему приложению, в нем мы хотим добиться следующих целей:
синхронизировать фотографии с сервером без аффекта на UI (done! используем для этого Background Context в стэке);
— на главном экране показывать все фото, отсортированные по дате;
— на вторичном экране группировать фото, где критерии группировки — число лайков, фото внутри группы дополнительно сортируются по дате.

Подойдем для начала к решению задачи в лоб, создадим модель, в которой будет всего одна Entity — наша фотография со всей метаинформацией:

First model

Получилось очень просто, и, будь мы ленивыми разработчиками, на этом работа была бы закончена (и статью писал бы кто-то другой :)).

Для тестирования мы будем предполагать, что на главном экране мы нам потребуется простой NSFetchRequest, результаты которого мы затем покажем в UICollectionView:

NSFetchRequest

А на дополнительном экране мы воспользуемся всей мощью NSFetchedResultsController для формирования секций и сортировки в них:

NSFetchedResultsController

Определившись с нашей моделью, сделаем контрольный замер производительности на iPhone 5 для 10000 фото. Здесь и далее мы будем производить тестирование нашей модели на типичные операции, связанные с нашей моделью:
  • Вставка 10000 объектов с последующим сохранением контекста
  • Реквест всех 10000 объектов, отсортированных по одному полю (в нашем случае дата)
  • Использование NSFetchedResultsController c сортировкой по 2 полям и формированием секций (сортировка по количеству лайков и дате, формирование секций по количеству лайков)
  • Все тот же контроллер с использованием fetchBatchSize равным 30 (предполагаемое количество фотографий на экране галереи на телефоне), для оценки эффективности блочной выборки данных

Все данные в таблицах приведены в секундах, соответственно вставка 10000 наших фотографий на iPhone 5 займет чуть меньше двух секунд.
Операции\Тип модели Модель V1
Insets (10000 objects) 1.952
NSFetchRequest (1 sort) 0.500
NSFetchedResultsController (2 sorts) 0.717
NSFetchedResultsController (2 sorts + batchSize) 0.302

Индексы

Хотя время исполнения может показаться несущественным, не стоит пренебрегать возможностью оптимизации. Более того, на старых устройствах операции выполняются в разы медленнее, и забывать об этом точно не стоит. Первая оптимизация самая легкая и известна каждому — попробуем добавить индекс для полей, которые участвуют в формируемых нами запросах, а именно date и likes:
Операции\Тип модели Модель V1 V1+индекс Diff
Insert (10000 objects) 1.952 2.193 +12%
NSFetchRequest (1 sort) 0.500 0.168 -66%
NSFetchedResultsController (2 sorts) 0.717 0.657 -8%
NSFetchedResultsController (2 sorts + batchSize) 0.302 0.256 -15%

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

Все ли соки мы выжали из индекса? Можно заметить, что NSFetchedResultsController «ускорился» значительно в меньшей степени, чем простой NSFetchRequest. В чем же дело?

Составные индексы

Давайте заглянем под капот CoreData. В первую очередь для этого нам нужно включить лог для Core Data запросов, добавив в Run схему нашего проекта параметр " -com.apple.CoreData.SQLDebug 1" как на рисунке:

Debug

Далее нам необходим файл sqlite хранилища в его наполненном состоянии. Если вы работаете с симулятором, то Хcode 6 хранит файловую систему симуляторов в директории "~/Library/Developer/CoreSimulator/Devices/". Название директории симулятора соответствует значению Identifier, который можно посмотреть в списке устройств (открывается по Shitft+CMD+2). Далее ищем директорию своего приложения и узнаем полный путь до .sqlite файла, который обычно размещают в директории Documents приложения. Если же вы хотите получить доступ к хранилищу на устройстве, то самый простой способ воспользоваться приложением iExplorer, используя его как файл-менеджер для просмотра директорий приложений на вашем девайсе. Оттуда вы можете скопировать файлы хранилища (не забывайте про .sqlite-wal и .sqlite-shm файлы) в любую удобную вам директорию. Все, что осталось сделать — это подключиться к нашему хранилищу из консоли, выполнив команду:

sqlite3 PATH/TO/SQLITE/FILE

Теперь, запустив наш проект и скормив SQL директиве "EXPLAIN QUERY PLAN" запрос из логов Core Data, мы можем узнать некоторые подробности происходящих в sqlite процессов. Посмотрим, что же происходит на самом деле при выполнение NSFetchRequest:

sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZASSETURL, t0.ZCOUNTRY, t0.ZDATE, t0.ZHEIGHT, t0.ZLATITUDE, t0.ZLIKES, t0.ZLOCATIONDESC, t0.ZLONGITUDE, t0.ZSIZE, t0.ZWIDTH FROM ZCDTMOPHOTOV1INDEX t0 ORDER BY t0.ZDATE;
0|0|0|SCAN TABLE ZCDTMOPHOTOV1INDEX AS t0 USING INDEX ZCDTMOPHOTOV1INDEX_ZDATE_INDEX

Как и ожидалось SQL-запрос использует индекс, что и привело к существенному ускорению. А что же происходит в NSFetchedResultsController:

sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZASSETURL, t0.ZCOUNTRY, t0.ZDATE, t0.ZHEIGHT, t0.ZLATITUDE, t0.ZLIKES, t0.ZLOCATIONDESC, t0.ZLONGITUDE, t0.ZSIZE, t0.ZWIDTH FROM ZCDTMOPHOTOV1INDEX t0 ORDER BY t0.ZLIKES DESC, t0.ZDATE DESC;
0|0|0|SCAN TABLE ZCDTMOPHOTOV1INDEX AS t0 USING INDEX ZCDTMOPHOTOV1INDEX_ZLIKES_INDEX
0|0|0|USE TEMP B-TREE FOR RIGHT PART OF ORDER BY

Тут дела обстоят несколько хуже, индекс сработал только для likes, а для сортировки по дате создается временное бинарное дерево. Оптимизировать такое поведение легко, создав составной индекс (compound index) для обоих полей участвующих в запросе (CAUTION: если в вашем запросе появится дополнительное условие, например WHERE, с каким-то третьим полем, то его также необходимо добавить к составному индексу, иначе он не будет использоваться при запросе). Делается это очень легко в Data Model Inspector, указав через запятую все поля, включаемые в составной индекс, в списке Indexes нашей фото Entity:

Data Model Inspector

Посмотрим, как теперь будет обрабатываться SQL-запрос:

sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZASSETURL, t0.ZCOUNTRY, t0.ZDATE, t0.ZHEIGHT, t0.ZLATITUDE, t0.ZLIKES, t0.ZLOCATIONDESC, t0.ZLONGITUDE, t0.ZSIZE, t0.ZWIDTH FROM ZCDTMOPHOTOV1COMPOUNDINDEX t0 ORDER BY t0.ZLIKES DESC, t0.ZDATE DESC;
0|0|0|SCAN TABLE ZCDTMOPHOTOV1COMPOUNDINDEX AS t0 USING INDEX ZCDTMOPHOTOV1COMPOUNDINDEX_ZLIKES_ZDATE

Можно убедиться, что вместо бинарного дерева используется составной индекс, и это не может не сказаться на производительности:
Операции\Тип модели Модель V1 V1+индекс V1+составной индекс Diff(V1)
Insert (10000 objects) 1.952 2.193 2.079 +7%
NSFetchRequest (1 sort) 0.500 0.168 0.169 -66%
NSFetchedResultsController (2 sorts) 0.717 0.657 0.331 -54%
NSFetchedResultsController (2 sorts + batchSize) 0.302 0.256 0.182 -40%

Разбиение сущностей

Еще одна возможность для оптимизации — создание сущностей, которые содержат только нужную нам в конкретном запросе информацию. Мы видим, что наша структура содержит множество второстепенных полей, никак не участвующих в формировании выдачи первоначального результата в наших контроллерах. Более того, Core Data при работе с объектом полностью вытягивает их в память, то есть чем больше структура, тем больше потребляемой памяти (прим. в iOS 8 появилось API, позволяющее изменять объекты прямо в хранилище; API довольно ограничено в использовании, так как накладывает дополнительные требования на синхронизацию контекстов). В нашем приложении само собой напрашивается разделение нашей записи на две: сама фотография и метаданные для нее:

Separated entities

Проведем очередной тест и посмотрим на работу индексов для такой модели.
Операции\Тип модели Модель V2 V2+индекс Diff(V1+индекс)
Insert (10000 objects) 3.218 3.524 +61%
NSFetchRequest (1 sort) 0.219 0.215 +28%
NSFetchedResultsController (2 sorts) 0.551 0.542 -18%
NSFetchedResultsController (2 sorts + batchSize) 0.387 0.390 +52%

Why is your index now?

Результаты этого теста довольно интересны. Обратите внимание, что скорость данной модели с использованием индекса идентична с учетом погрешности модели без него. Воспользовавшись уже известным нам способом заглянуть вглубь, мы можем обнаружить, что в обоих случаях индекс не задействуется, поскольку первым происходит JOIN метаданных, и только потом производятся сортировки в объединенной таблице:

sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZASSETURL, t0.ZMETA FROM ZCDTMOPHOTOV2INDEX t0 LEFT OUTER JOIN ZCDTMOPHOTOMETAINDEX t1 ON t0.ZMETA = t1.Z_PK ORDER BY t1.ZLIKES DESC, t1.ZDATE DESC;
0|0|0|SCAN TABLE ZCDTMOPHOTOV2INDEX AS t0
0|1|1|SEARCH TABLE ZCDTMOPHOTOMETAINDEX AS t1 USING INTEGER PRIMARY KEY (rowid=?)
0|0|0|USE TEMP B-TREE FOR ORDER BY

Итог: эта модель нам не подходит.

Нормализация

Продолжаем наши эксперименты. Мы убедились, что строгая нормализация данных не всегда есть хорошо для Core Data. Результаты прошлой модели оказались далеки от ожидаемых. Попробуем это исправить. Для этого достаточно продублировать наши поля date и likes в сущности фотографии (не забыв добавить составной индекс и отдельный для date), тем самым избежать необходимости LEFT OUTER JOIN в наших запросах. Решение оставлять или удалять эти поля в сущности метаданных нужно принять в зависимости от ситуации. Например, если дополтительно вы захотите сделать запрос с рейтингом стран по сумме лайков фотографий, сделанных в них, то при удалении этих полей мы опять столкнемся с необходимость делать JOIN, но уже в другую сторону связи. В нашем тесте свойства сущностей дублируются, и это совершенно нормальное являение для Core Data:

Third model

Посмотрим на результаты тестирования:
Операции\Тип модели Модель V3 Diff(V1+составной индекс) Diff(V1)
Insert (10000 objects) 3.861 +86% +98%
NSFetchRequest (1 sort) 0.115 -32% -77%
NSFetchedResultsController (2 sorts) 0.283 -15% -61%
NSFetchedResultsController (2 sorts + batchSize) 0.181 -1% -40%

Эксперимент удался, мы добились ускорения операций чтения, которые являются основными в приложении до 40% в сравнении с самой быстрой плоской моделью и до 80% с первоначальным вариантом без индексов.

Итоги и тонкие моменты

  • Применяйте индексы и используйте их только для актуальных в ваших запросах полей. Не забывайте о существование составных индексов
  • Экспериментируйте с разными схемами, тестируйте их производительность. Это очень просто, ведь в Xcode 6 появилась встроенная поддержка perfomance тестов.
  • Не забывайте проверять как CoreData фреймворк генерирует SQL-запросы, используя логи. С помощью "EXPLAIN QUERY PLAN" изучайте как sqlite переваривает ваш SQL запрос.
  • При обращении к результатам NSFetchedResultsController используйте только метод доступа, предоставляемый самим контроллером:

    NSManagedObject *object = [controller objectAtIndexPath:indexPath];
    

    Не стоит обращаться к массиву fetchedObjects или по протоколу NSFetchedResultsSectionInfo к массиву объектов секции:

    NSManagedObject *object = [[controller fetchedObjects] objectAtIndex:index];
    // или
    NSArray *objects = [[[controller sections] objectAtIndex:sectionIndex] objects];
    NSManagedObject *object = [objects objectAtIndex:index];
    

    Почему, спросите вы? Если вы используете fetchBatchSize размером N, то после выполнения запроса контроллером в память будет загружено только первые N объектов (либо первая секция, если размер блока больше размера секции!). Как только вы запросите первый fault-объект за пределами загруженного блока или объект из другой секции, то контроллер произведет полный проход по результатам вашего запроса, то есть выполнит N=количествоОбъектов / fetchBatchSize запросов к хранилищу. Это операция приблизительно в 3-4 раза медленнее, чем простой запрос на все элементы. При использовании доступа через objectAtIndexPath такое поведение не наблюдается. Буду очень рад, если среди читающих найдется кто-то, кто может пролить свет на такое странное поведение, не описанное в документации.
  • Нормализация — не всегда лучшее решение для Core Data
  • Если со сцены в Купертино вам говорят о том, что новый iPhone в 2 раза быстрее предыдущего… нужно этому верить, на Core Data операциях эти утверждения подтверждаются практически полностью. Я подготовил сводный файл с результатами, где вы также найдете тесты iPhone 5S. Практически по всем результатам он быстрее своего предшественника в 2 раза. Соответвенно, на еще актуальном iPhone 4S эти результаты будут примерно в 2 раза медленнее, не говоря уже о еще более старых устройствах. Здесь вы найдете сводную таблицу результатов, в которой также есть результаты нового iPhone 6.


Как вы можете заменить, Core Data является не только простым средством работы с данными, но и мощным инструментом в умелых руках. Исследуйте и экспериментируйте, а я надеюсь, что статья открыла для вас что-то новое и подтолкнула навстречу к более эффективному применению Core Data в своих проектах. Удачи!
Автор: @WisDooMer

Комментарии (27)

  • +1
    Спасибо, интересная статья.
    Как раз начал использовать CoreData.
    • +1
      Не стоит обращаться к массиву fetchedObjects или по протоколу NSFetchedResultsSectionInfo к массиву объектов секции:


      А для получения кол-ва объектов можно ведь использовать протокол NSFetchedResultsSectionInfo?
      Пример для вывода кол-ва элементов для collectionView:
      return [[([_fetchResultsController sections] count)] numberOfObjects];
      • 0
        Этот метод можете смело использовать, он не вызывает полного рефетча данных.
  • 0
    Спасибо!)
  • 0
    Спасибо! Много интересного даже для тех, кто давно использует Core Data :)
  • 0
    Удивляет меня скорость записи. Загрузить и распарсить json получается заметно быстрее, чем записать результат в Core Data. Чем это обусловлено?
    • +1
      Испокон веков завелось, что read быстрее write. Часть времени съедается на построение индекса(как минимум primary index), часть на I/O с постоянной памятью. Обычные приложения, направленные на потребление контента, большую часть времени занимаются read и write можно пренебречь. Если же ваше приложение критично к записи, то вам противопоказано разделение сущностей.
      • 0
        Понимаю, что запись чисто физически должна работать медленней, однако разброс 3 секунды и доля секунды всё-же не укладывается в голову.
        • 0
          10к записей! Вы реально телефоном 10к записей планируете инсертить?
          • 0
            Что фантастичного в 10к записей? Простой кейс — облачное хранилище фотографий, коих у юзера вполне может быть и больше, и соответсвенно initial sync на свежей установке.
            • 0
              Фантастичны не 10к записей, фантастичен такой initial sync: зачем тащить сразу все, если можно дать пользователю что-то делать с первичкой, а тащить все фоном и то если такое действительно необходимо. Да и шансы сбоя на 10к записях такие большие, что лучше дробить по 1к хотя бы, чтобы в случае любого сбоя не начинать сначала.
              • 0
                В реальности все так и происходит по блокам, тестирование лишь определяет average time per insert, который для нас критичен.
  • +1
    Не могу понять почему так популярны такие сложные структуры с контекстами в CoreData для работы в разных потоках. К примеру приведенная в этой статье или еще круче: stackoverflow.com/questions/21476559/coredata-child-contexts-nsfetchedresultscontroller-and-main-thread. Я считаю что обращаться к CoreData можно только в фоновом потоке. Простая модель позволяет минимизировать что делается под капотом, чтобы было легче контролировать объемы используемой памяти и cpu. Да, NSFetchResultController я не использую.

    Работа с базой у меня идет через отдельный объект. Прочтенные Entity из базы котвертируются в NSObject производные объекты c теми-же полями и передаются в главный поток уже с помощью делегата или нотификации. Там они уже отображаются в кастомном UI (типа collectionView).

    Буду рад комментариям по поводу моего подхода работы с CoreData.
    • 0
      Лишнюю сложность в проектировение стеков я отметил и в своей статье. Действительно, нужно исходить из требований. Если у вас приложение, где вы одновременно работает с 1-2 записями, а максимальное их количество не превышает скажем тысячи, то вам нет особого смысла вообще усложнять себе жизнь работой с бэкграундом, Core Data достаточно быстра, чтобы совладать с такими масштабами прямо на главном контексте.

      Ваш подход мне кажется своебразным, но интересным. Мне хотелось бы узнать подробности как вы обрабатываете fault для ваших NSObject объектов и пагинируете большой объем данных? А также интересно как вы реагируете на изменения в контексте? Возможно это материал для написания статьи? Я бы с удовольствие почитал о вашем опыте. ;)
      • 0
        В моем случае объектов может быть сколько угодно и размер базы зависит от пользователя. По сути это список который мне нужно отображать в одном контроллере, обрабатывать добавление (по мере загрузки данных), удаление и пейджинацию. Manager работающий с базой хранит все методы чтобы сохранить в нее NSObject объекты и взять их. Внутри эта работа проецируется на CoreData запросы которые выполняются на одной queue через dispatch_async поэтому fault-ов не происходит. Контекст в итоге всего один, с ним я в потоке и работаю. Пейджинация происходит как вызов метода у manager-а c указанием необходимого диапазона записей, данные приходят в переданный completionBlock-e. Данные сортируются по дате. Если где-то добавляется объект в manager то он нотифицирует об этом передавая обернутый объект после записи в базу. Про статью не уверен, вроде все тут уместилось).

        • +1
          Меня еще терзают сомнения, стоит ли упрощенный стек этих ухищрений с модельно, но тем не менее мое мнение выражается в том, что любой подход имеет право на жизнь, если он работает и его работа всех устраивает :) Попробуйте, кстати посмотреть на методы performBlock: и performBlockAndWait: контекста, с ними не нужно реализовывать свою очередь.
    • 0
      Если вы не используете NSFetchResultController, и везде у вас свои модели, то зачем вам вообще Core Data? Почему бы не общаться напрямую с SQLite используя тот же FMDB? И лишнее звено в цепочке ущйдёт, и возможности SQLite можно будет использовать по полной программе.
      • 0
        Ну все-же плюсы CoreData очень полезны и для меня: автоматическая миграция, визуальное создание таблиц, простое оперирование с объектам и их связями между собой (автоматическая подгрузка при обращении к ним). С CoreData я думаю все-же проще работать. Я вообще не рассматривал чистый SQLite подход, возможно стоит попробовать), сразу сказать сложно.
  • 0
    прим. в iOS 8 появилось API, позволяющее изменять/удалять объекты прямо в хранилище;

    API по изменению видел, но что нового с удалением? Разве появился способ удалить большое множество объектов, не загружая при этом их все в память, и не высасывая неявно все каскадные зависимости, которые тоже надо удалить?
    • 0
      Оу, большое спасибо за замечание. До нового API еще толком не дошли руки, а WWDC видео было просмотрено уже довольно давно, что-то в памяти значит каратнуло. Действительно, новое API пополнилось NSBatchUpdateRequest, название которого говорит само за себя.
  • 0
    Когда прочитал «Для этого нам потребуется Core Data… и все.» подумал, что вы и сами фотографии будете хранить в Core Data…
    p.s. Был печальный опыт переписывания кода, который кешировал все http запросы к картинкам в core data, а потом спрашивали, почему так лагает прокрутка галереи со 100+ миниатюрами.
    • 0
      Понимаю вашу боль. Хотя сама Apple в своих сессиях говорит о том, что миниатюры, хранимые в Core Data — это OK, меня терзают на этот счет смутные сомнениями… тем более, что сами они хранят миниатюры в EXIF'ах фоток.
  • 0
    Core Data прекрасная вещь, которая при должном понимании деталей и методов работы через разные потоки превращается в крайне мощный инструмент.

    Другое дело это iCloud Core Data, которая даже в iOS 7 была еще настолько ненадежна, что могла по итогу запороть любой проект.
  • 0
    Статья интересная, спасибо!
    Реальных use cases для Core Data по пальцам пересчитать. А в свете последних тенденций ухода в сторону immutable сущностей, она становится все сложнее и сложнее для понимания.
  • 0
    Core Data это хорошо, но слишком много «магии» под капотом. К тому же после N выпущенных версий приложения, миграции схемы занимают просто дикое количество времени.
    Лично я открыл для себя Realm, и плотно экспериментирую с immutable сущностями (примерно как это делают в Facebook).
  • 0
    Хотел уяснить для себя такой момент. В документации Apple и в разных примерах инициализация стека Core Data суюют в Appdelegate.
    Создал для себя отдельный объект синглтон, свойсвтами которого есть managedObjectContext, FetchedResultsController и тд. Тоесть приложение все дейсвтия с кор датой делает через этот объект.
    Вопрос такое — если моя модель использует 2 Entity — как сделать разграничение и создавать 2 fetchedresultscontrollerа в этом объекте? как разделять обращение к тому или другому?
    заранее спасибо
    • 0
      NSFetchedResultsController — это не часть Core Data стэка, не нужно его хранить в нем. Если ваши данные представлены таблицей, то вам нужно использовать этот контроллер как обычный табличный контроллер, если в другом виде, то в ваше контроллере экрана просто создаете новый NSFetchedResultsController на контексте в главном потоке например в viewDidLoad и используете его данные для показа. Разграничивать 2 таких контроллера не надо, все будет сделано за вас внутри.

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

Самое читаемое Разработка