Лидер мобильной разработки в России
120,21
рейтинг
17 октября 2013 в 15:50

Разработка → Сетевое кэширование в iOS. Введение

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

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

Итак, введение.


Стратегии для кэширования


Есть два подхода к кэшированию: кэширование по требованию и предварительное кэширование.

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

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

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

Где хранить кэш


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

В этом примере в папке Library/Caches создается директория MyAppCache:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDirectory = [paths objectAtIndex:0];
cachesDirectory = [cachesDirectory stringByAppendingPathComponent:@”MyAppCache”];

Причиной хранения кэша в папке Library/Caches является то, что iCloud (и iTunes) исключает эту директорию из бэкапа. И, следовательно, и так ограниченное в iCloud пространство (в настоящее время для бесплатного аккаунта, это около 5 GB) не тратится на хранение ненужных данных.

В случае если в приложении происходит интенсивное кэширование, рекомендуется вместо диска использовать память и выгружать данные на диск при закрытии приложения. Это связано с тем, что flash память iPhone имеет ограниченное число циклов записи/чтения и нежелательно нагружать ее лишний раз.

Как хранить кэш


На iOS существует множество различных способов хранения пользовательских данных. Для кэширования лучше всего подходят: NSKeyedArchiver, Core Data, SQLite, NSURLCache.

NSKeyedArchiver


Кэширование модели данных реализуется с использованием класса NSKeyedArchiver. Для того, чтобы объекты модели могли быть архивированы, классы модели должны реализовать протокол NSCoding. А именно методы
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;

Если класс реализует NSCoding, для архивации достаточно вызвать один из следующих методов:
[NSKeyedArchiver archiveRootObject:objectForArchiving toFile:archiveFilePath];

[NSKeyedArchiver archivedDataWithRootObject:objectForArchiving];

Первый метод создаст файл с архивом по пути archiveFilePath. Второй метод вернет объект NSData. NSData обычно быстрее, так как отсутствуют дополнительные затраты на доступ к файлу, но при этом данные будут храниться в памяти приложения.

Для разархивирования модели из файла (или указателя на NSData) используется класс NSKeyedUnarchiver. Разархивировать данные можно одним из следующих методов:
[NSKeyedUnarchiver unarchiveObjectWithData:data];

[NSKeyedUnarchiver unarchiveObjectWithFile:archiveFilePath];

Использование NSKeyedArchiver/NSKeyedUnarchiver требует чтобы модели удовлетворяли протоколу NSCoding. Реализация NSCoding очень проста, но если файлов много, то она может занять много времени. Поэтому для автоматизации этого процесса лучше использовать какой-либо инструмент. Например среду разработки AppCode.

Core Data


Чтобы хранить данные в Core Data, необходимо создать файл модели, который содержит описание сущностей (Entities), а также связей между ними (Relationships), и написать методы для сохранения и получения данных. Используя Core Data можно получить настоящий offline режим работы приложения, как это сделано в стандартных приложениях Mail и Calendar.

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

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

Raw SQLite


Для работы с SQLite надо слинковать приложение с библиотекой libsqlite3, но такой подход имеет значительные недостатки.
Все sqlite3 библиотеки и механизм Object Relational Mapping (ORM) работают медленнее чем Core Data. Кроме того реализация sqlite3 в iOS не потоко-безопасна. Так что если вы не используете отдельно собранную sqlite3 библиотеку (скомпилированную с флагом thread-safe), то вы сами отвечаете за то, чтобы гарантировать потоко-безопасный доступ на чтение/запись к базе данных sqlite3.

Так как Core Data может предложить гораздо больше возможностей (миграция данных, встроенная потоко-безопасность, ...) рекомендуется избегать использование нативной SQLite на iOS.

NSURLCache


Идеальный вариант для кэширования по запросу. Позволяет кэшировать данные возвращаемые NSURLRequest практически в автоматическом режиме и требует минимум кода.

Всего лишь пара строк и ваше приложение получит дисковый кэш для запросов.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 
                                                       diskCapacity:20 * 1024 * 1024 
                                                           diskPath:nil];
  [NSURLCache setSharedURLCache:URLCache];
}


К сожаление пригоден только для REST сервисов и есть проблемы при работе с некоторыми HTTP заголовками.

Заключение


Для реализации кэширования по запросу лучше использовать NSURLCache или NSKeyedArchiver. Реализация полноценного offline режима требует работы c CoreData.

В следующей части я планирую детально рассмотреть работу с NSURLCache/NSKeyedArchiver и описать случаи, для которых они подходят.
Автор: @Fanruten
e-Legion Ltd.
рейтинг 120,21
Лидер мобильной разработки в России

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

  • 0
    При обработки больших баз данных (более 10000 записей) не рекоммендуется использовать Core Data. Для таких целей можно использовать, например, FMDB
    • 0
      Еще при помощи FMDB можно выдирать данные из дискового кэша NSURLRequest.
    • 0
      Тут скорее не в количестве записей дело, а в сложности модели данных.

      Как раз тупо показывать плоский список из > 10000 записей проще с помощью NSFetchedResultsController.
  • 0
    Разве CoreData обычно не использует SQLLite для хранения данных?
    • 0
      Обычно его и использует. Но CoreData сразу позволяет работать с объектной моделью данных, а не записями. И в ней реализованы механизмы, позволяющие не считывать всё подряд, а брать только необходимые данные. При чём «на лету» и прозрачно — т.е. можно не грузить объект целиком, а только те его свойства, которые нужны. И загружать всё остальное без всяких усилий со стороны программиста.

      Это обеспечивает выигрыш в скорости, по сравнению с чистым SQLite
      • 0
        как это может обеспечивать выигрыш, если на чистом sqlite можно сделать то же самое, пусть и не прозрачно для программиста?
        • 0
          Даёт выигрыш — в смысле при сопоставимых временных затратах. Разумеется, в принципе можно сделать и побыстрее, под свою задачу. Можно, но нужно ли на это тратить время? Core Data используют в достаточно сложных проектах, в простых нету смысла возиться с её заморочками. Реализация необходимого для такого проекта функционала может потребовать значительного времени и нет гарантии что не придётся многократно усложнять и оптимизировать. Чтобы в итоге получить свою Core Data, только дорого, долго и необкатаную.

          В общем, встречал упоминания, что при переходе с SQLite на Core Data производительность возрасла. Обратных случаев не припомню.
          • 0
            У меня был случай когда ради определенной функциональности пришлось начать использовать отдельныое SQLite-хранилище, параллельно с основным Core Data, потому что тормозило (могу объяснить что и как тормозило, если интересно)
            • 0
              Интересно.
              • 0
                В модели была m2m-связь (далее просто «связь») «картинка — сущность». Картинки периодически надо было удалять, если они более не использовались сущностями с определенным флагом (ну например, картинка не используется в избранных постах).

                Так вот, чтобы удалить одну картинку, Core Data вынуждена была грузить все связанные с ней сущности, чтобы удалять картинку из их коллекций «images». Это было медленно и печально.

                Я видел два варианта решения:

                • Сделать связь односторонней: это приводило к ряду проблем в логике, также ответственность за целостность ложилась на меня
                • Хранить связь в отдельном SQLite-хранилище


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

                В итоге все же реализовал вторым способом, обрабатывая уведомления о изменении NSManagedObjectContext и сохраняя нужную информацию в SQLite. Упоминаемая связь в Core Data стала атрибутом сущности, содержащим сериализованный NSArray (список идентификаторов картинок), SQLite же хранит полноценную двустороннюю связь.

                Теперь когда надо удалить картинку, достаточно:

                • Удалить ни с чем не связанный объект картинки из Core Data (быстро)
                • Выполнить простой SQLite-запрос для удаления связи (очень быстро)
                • 0
                  Ну, а во втором варианте решения ответственность за целостность также целиком и полностью на вас… Или я что-то не так понял? Был еще вариант нагло полезть в базу нативным sqlite, разобраться со структурой и руками написать пару соответствующих запросов.
                • 0
                  Интересный случай, но поддержу вопрос: зачем вводить новую сущность (SQLite хранилище), если это не решило принципиального вопроса — необходимость заботиться о целостности данных.
                  • 0
                    Принципиальной проблемой все же была производительность.

                    И нарушение целостности при таком подходе не так опасно, как нарушение целостности в Core Data-хранилище.
                    • 0
                      Односторонние связи работали сопоставимо с SQLite, или заметно медленнее? Почему не стали использовать этот подход?
                      • 0
                        В SQLite я мог позволить себе хранить нормальную двустороннюю связь. Использование односторонней связи в Core Data усложнило бы код (т.к. для ряда юзкейзов нужно было одно направление связи, для других — другое) и увеличило бы вероятность где-то облажаться и постучаться к несуществующему объекту.

                        Вообще я не утверждаю что мое решение идеально, сроки тогда поджимали, пришлось решать проблему с производительностью вот так. Возможно сейчас я бы выбрал другой вариант.
  • 0
    Данные в Library/Caches могут быть удалены системой — т.е. пользователь может лишиться своего кэша внезапно. Для разработчиков игр например это критично, да и как пользователю мне лично не приятно.

    Поэтому все же лучше хранить в папке приложения, просто на папку с кэшом ставить флаг, что ее не нужно синхронизировать с iCloud.
    • +4
      Многие программисты плохо представляют себе, что такое кэш, кэш — это данные, которые программа может легко потерять без ущерба для функциональности, кэш нужен для увеличения скорости работы, уменьшения времени отклика и экономии трафика при повторной загрузке контента.

      Если программа «кэширует» данные для возможности работы в офлайне — это не кэш, это сохранённые для просмотра в офлайне данные.
      Если игра после установки докачивает «кэш» необходимый для нормальной работы игры, это не кэш, это DLC.
      Если браузер при загрузке Хабра сохраняет локально графический файл с логотипом, чтобы при повторном открытии Хабра не грузить его из интернета, это кэш.
      Если программа офлайновых карт «кэширует» карты Франции или Новочебурашкина, это не кэш, это сохраняемые пользователем данные, потерей которых он вряд ли будет обрадован.
      Если Хабраридер сохраняет локально скриншоты в статьях, это кэш.
      Если Хабраридер сохраняет топики для прочтения в офлайне (вместе со скриншотами), это не кэш.

      Если это однажды понять, то потом не будет мучительно больно, когда коллега после небольшого изменения в своём коде вдруг грохнет папку /cache на сервере, а вы там хранили сессионные данные (в лучшем случае) или загруженные пользователями фотографии, и тысячи пользователей получат непонятные ошибки или лишатся своих фоточек, или когда iOS при нехватке места вдруг удалит сохранённые для просмотра в офлайне записи или скачанные игрой текстуры, или закэшированные для офлайна карты, а пользователи наставят минусов приложению, потому что собирались им воспользоваться во время отпуска/перелёта/поездки на дачу и прилежно забили свой гаджет под завязку.
      • 0
        Кэш браузера хороший пример.

        Если система, без моего уведомления почистит мне кэш бразуера (которым управлет замечу именно браузер, а не система — именно он позволяте мне выставить размер, и даже в ряде экзотических браузеров говорить что кэшировать, а что нет) — то это значит, что браузер сохраняет кэш куда-то нетуда. С таким же успехом он мог сохранять свой кэш в темп.

        И как бы моей операционке бы не хватало места, оно никогда без моего ведома не почистит мне кэш браузера.

        И это правильно. Ибо потом я приду туда где интернет 10 килобит и пинги по 4 секунды и очень растроюсь без кэша.

        Поэтому хотите называйте это кэше, хотите DLC, хотите сохраняемыми пользователями данные — не важно. Важно, то что размешать такие данные в место которое может быть без уведомления программы которая данный создала (а уж тем более пользователя) в корне не верно.

        Вот вы пишите

        Если Хабраридер сохраняет локально скриншоты в статьях, это кэш.

        И как я понимаю, предлагаете сохранять в Library/Caches из-за чего пользователь может получить следующе поведение — зашел в статью, загрузил скриншоты, зашел еще раз они загрузились быстро, зашел еще раз никуда не заходя — загрузились медленно.

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

        С играми, чтобы упростить пример, вот вам пример, у нас в игре есть встроенный бразуер на вебвью который позволяет ходить по порталу в игру (никакого отличия от хабраридера же?) — каждый раз когда там грузится странчика, загрузки всех картинок перехватываются и они кладуться пользователю на диск. В вашем понимании это кэш. А в моем понимании я хочу, чтобы у пользователя даже через полгода при открытии портала в игру все грузилось быстро — что гарантировать размещая данные в Library/Caches нельзя.

        Т.е. может в Хабраридер можно позволить так поступать с пользователями, а нам нет.
        • +1
          Именно о таком непонимании я и написал.

          Выставьте вы хоть гигабайт для кэша в браузере — когда-нибудь он переполнится, и новые файлы начнут вытеснять старые. А значит приехав «туда где интернет 10 килобит» вы внезапно окажетесь без дорогих вам файлов в кэше.

          Кэш браузера — хороший пример. Может так случиться, что он займёт всё свободное пространство (айпад 16гб, например, постоянно не хватает места), а я вдруг решу записать фильм себе в дорогу. Умная система пойдёт и подчистит кэши в приложениях, и мой фильм поместится. Иначе мне придётся самостоятельно перебирать зажравшиеся приложения, копаться в их настройках подчищая кэши или вовсе их сносить.

          Ваш пример с игрой: если не предусмотрено «хождение по порталу в игру» в офлайне, то да, это кэш. И пока свободного места достаточно, то пусть себе лежит, но когда места станет не хватать, то пусть ОС подчистит ненужные кэши. Или вы предпочитаете, чтобы пользователь удалил ваше приложение увидев, что оно чрезмерно раздулось от «хождения по порталу в игру»? Если же вы хотите, чтобы пользователь мог в офлайне ходить по порталу в игру, то это не кэш, конечно.

          Кэш — это то, без чего приложение будет работать, пусть и медленнее.

          К сравнению с тэмпом: тэмп «внезапно» чистить нельзя — там могут лежать файлы, с которыми программа работает в данный момент, например, это может быть промежуточный результат перекодирования файла. Кэш можно чистить в любой момент.
          • 0
            Когда кэш переполнится, во первых «новые» файлы начнут замешать «старые» по вполне конкретном алгоритму который учитывает кучу всего, и заточен именно на наше приложение. Ибо мы определяем, и размер кэша и как его чистить и что замещать. Исходя из структуры данных о которой мы знаем.

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

            А вот любители сохранять в /Library/Caches как правило не заморачиваются не умным замещением файлов, ни политикой кэширования в зависимости от заполнености девайса и прочее, прочее…

            А по поводу примера, вот у нас игра (онлайновая) может полностью работать через streaming — т.е. в целом даже если места на девайсе 0, то все равно все текстуры будут скачиватся, загружатся в память и игрок сможет играть хоть и медленее. Т.е. это нефига не DLC — т.е. это ресурсы которые необходимы для игры. Но при этом если, место на диске есть — то они будут кэшироваться. И заменятся на апдейте при сервере и т.п.

            Т.е. никакой разницы между текстурами и данными в игре в нашем случае, и картинками на сайте я не вижу.

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

              Видимо, у вас своя специфика — с играми через streaming я не сталкивался.
              Зато сталкивался с использованием кэша для данных, которым в кэше не место — и как разработчик, и как пользователь.
      • 0
        Я вообще не могу придумать примеров когда пользователю будет лучше, что данные сохраняются в Library/Caches если честно.

        Ну разве, что речь идет о данных которые живут в пределах одной сессии приложения, да и то на мой взгляд для пользователя _внезапная_ потеря таких данных без уведомления будет странноватой.
        • 0
          Насколько я знаю, «внезапно» ОС кеши не очистит, только при неактивном приложении.

          Ну и я предпочту очищенные кеши, чем ошибку нехватки места на устройстве при сохранении чего-нибудь (которую программа еще непонятно как обработает).
          • 0
            Данные в /Library/Caches могут быть почищены в _любой_ момент. Обратного никто не гарантирует.

            А ошибку нехватки места нужно обрабатывать вне зависимости от того что и куда сохраняется.

            Или вы полагаете, что если сохраняете в /Library/Caches и кончилось место, система удалит что-то и вам удастся прозрачно продолжить сохранение? Это нет так.
            • 0
              Данные в /Library/Caches могут быть почищены в _любой_ момент. Обратного никто не гарантирует.

              stackoverflow.com/questions/8918730/what-triggers-ios-5-0-cache-purge

              Ответ про iOS 5, но такие вещи без особой надобности не изменяют, поэтому можно полагаться на этот ответ в своих приложениях.

              А ошибку нехватки места нужно обрабатывать вне зависимости от того что и куда сохраняется.

              Все ли это делают?

              Или вы полагаете, что если сохраняете в /Library/Caches и кончилось место, система удалит что-то и вам удастся прозрачно продолжить сохранение? Это нет так.

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

              • 0
                Когда поменяют, вы узнаете только тогда когда у ваших пользователей перестанет что-то работать, если вы расчитывали на это поведение. Например перестанет работать ваша игры и вы начнете терять десятки тысяч долларов ежедневно.

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

                • –1
                  Когда поменяют, вы узнаете только тогда когда у ваших пользователей перестанет что-то работать

                  так подобное происходит после каждого мажорного релиза iOS. Не зря же разработчикам бета выдается за несколько месяцев до релиза.

                  На все потенциальные случаи изменения поведения ОС/SDK презервативы не придумаешь.
                  • 0
                    Вот с iOS7 менеджмент памяти (тригеры когда закрываются приложения в фоне и когда тебе memory warning кидают) — поменялось между GM и live-ом. Вот тебе на бетах все ок было, вышла лайвовая версия и настала жопа.
        • 0
          которые живут в пределах одной сессии приложения
          Как раз это и не кэш, хотя, не спорю, удобно воспользоваться для хранения сессионных данных готовым фрэймворком кэширования, а отдельную обёртку для хранения временных файлов писать лень :)
  • +1
    >> Главное преимущество Core Data заключается в предоставление доступа к свойствам модели без необходимости разархивировать все данные. Однако сложность реализации Core Data в приложении, перекрывает это преимущество.

    Вы не правы. Core Data очень просто готовить. Особенно это ощущается, когда надо модифить базу из нескольких разных тредов.
    • 0
      Просто, если использовать какой-нибудь фреймворк типа ObjectiveRecord .

      Иначе надо самому писать код для создания NSPersistentStore, NSManagedObjectContext и прочего. Плюс надо создавать модель данных, рисовать связи, иногда реализовать NSValueTransformer, заботиться о работе с базой из разных потоков.

      В то время, самая трудоемкая задача при использовании NSKeyedArchiver это реализация NSCoding, но при помощи AppCode ее можно решить нажатием трех кнопок.

      Так что, я за разумный выбор инструментов в зависимости от задачи )
      • 0
        А если использовать Mantle, то реализацию NSCoding можно получить искаропки.

        А код для настройки стека Core Data пишется один раз и делается компонентом.
  • 0
    Есть еще один потенциально интересный подход: github.com/AFNetworking/AFIncrementalStore

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

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