Добрый день, уважаемые хаброжители!

Images.xcassets даёт много приятных вещей, особенно мне нравятся Slicing и возможность добавить отдельную картинку для каждого типа устройства. Поэтому, начиная новый проект, я, даже не задумываясь, создаю Images.xcassets, добавляю туда иконки и splash, а потом и все новые ресурсы кидаю туда же. Сегодня я узнал пару неприятных вещей о xcassets, это заставило меня отказаться от него и потратить пару часов на переделку проекта. Не самое приятное и благодарное занятие. Если вы хотите учиться на чужих ошибках, прошу под кат.

Небольшая предыстория
С группой энтузиастов взялся делать сказки для детей на iPad. Заранее понимаю что на этом не разбогатеть, нашей целью является хорошее приложение, которое работает на всех девайсах с iOS 8. Так же, хотелось избежать до боли знакомой ситуации — вы скачали приложение, запускаете и узнаете, что для работы ему надо скачать еще >100МБ. Так что в приложение сразу хотелось сложить хотя бы одну книжку, чтобы после установки приложением можно было пользоваться. Книжка — это иллюстрации в формате jpeg, и без сжатия самая маленькая книжка весит 82Mb, остальные 100+Mb. А значит, если мы хотим оставить возможность скачивать его по 3g, на приложение у нас остается 18Mb. Это звучит невыполнимо, так как это детское приложение, и в нем много картинок, и большинство из них с орнаментом и узором, так что их ни ужать, ни порезать. Вот при таких изначальных данных я создал xcode проект.

Первый бой за память
Когда я начинал проект, у меня из тестовых девайсов был только iPad Air. На нём все летает, чего уже там греха таить — плохой выбор для тестирования. Поэтому, ближе к завершению проекта я приобрел iPad mini 1 gen — девайс без ретины и с процессором A5, но имеет iOS 8, а потом еще и iOS 9. При отладке приложения я время от времени получал memory warning. Не порядок, так быть не должно. Открываю «Debug» вкладку и вижу, что приложение откушало 100 Mb. И это при том, что я ничего особенного не делал, просто открыл приложение и имел в navigation stack всего 3 контроллера. Открываю профилировщик и вижу, что 70Mb занимают картинки. 100 Mb на iPad mini — явно перебор. Так что пришлось пойти и добавить большое число картинок со 1x scale (до этого их не добавлял, зная что для неретины iOS сама мне подготовит их), это, по идее, не большой удар по размеру приложения, но зато резко сократится обьем используемой памяти. В итоге я получил 47Mb. На мой взгляд, по-прежнему неоправданно, но зато перестали прилетать memory warnings.

Бой за размер
И вот вчера я закончил разработку приложения. Конечно, остались какие то TODOs, да и ребята его потестируют и, наверно, что-то найдут, но первая фаза закончена. Я на радостях выкатил билд остальным членам команды, заодно посетовал, что он весит 142Мб, а это значит, что книжку, которая вшита в приложение, нужно будет уменьшить в 2 раза по обьему, очень неприятный и заметный для глаза downgrade качества иллюстраций. Именно поэтому сегодня я решил заняться размером приложения и уменьшить его всеми правдами и неправдами, дабы не так сильно страдало качество.

Шаг 1 — посмотреть, что же занимает так много места
Беру build.ipa, переименовываю его в build.zip, разархивирую, и смотрю содержание build.app. Нахожу файл Assets.car, который весит 53Mb. В принципе, размер приложения объясним — 80 + 53 + по мелочам. Понимаю, что просто так его не раскрыть, решил посмотреть сам Images.xcassets в проекте. Она весит всего 38Mb. Это что же получается-то? Туда сложили что-то на 16Mb, а мне об этом сказать, видимо, забыли.

Шаг 2 — что же туда добавили-то?
Беглый поиск по просторам сети выдает мне cartool, для просмотра Assets.car. Делаю все по инструкции — собираю проект, беру собранный cartool и запускаю на моём Assets.car. Смотрю что у меня получилось в результате — 130 файлов общим размером 41.5Mb и, если 3.5Mb я могу списать на pngcrush, то куда ушли 11.5Mb?
Повторяю беглый поиск по сети. Нахожу подобные проблемы, к примеру, тут. И что я плачу-то из-за каких то 29%, там вообще в 6 раз вырос размер. Но это не мой случай — у меня все картинки png, так как почти все изображения имеют участки прозрачного фона. Больше я не нашел никакой надежды на исправление этой ситуации. Проверяю еще раз документацию Apple — они сделали бинарный файл, чтобы ускорить скачивание приложения. Как по мне, скорость скачивания напрямую зависит от размера. Поэтому я принял решение убрать все картинки из Images.xcassets и добавить их в проект по старинке. Мне пришлось в нескольких местах делать slicing в коде, используя
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets

Такими махинациями я смог сократить размер приолжения с 142Mb до 131Mb. Я однозначно продолжу оптимизацию ресурсов в приложении, но 10Mb за отказ от Images.xcassets уже неплохо.

Шаг 3 — что же стало с памятью?
Запустил на iPad mini — 23Mb против 47Mb. На iPad Air 66Mb против 100Mb.

Заключение
Может показаться, что разница между 142Mb и 131Mb невелика. Но это очень значительный показатель без ухудшения качества картинок и прочих трюков. Это значительный шаг к порогу в 100Mb и доступности приложения на 3G сетях. Да и сокращение памяти при использовании приложения — это очень приятный бонус.
Я не заметил особой разницы при старте приложения. Даже наоборот — я и так вставил задержку, чтобы пользователь подольше полюбовался на наш логотип. И впоследствии проблем со скоростью работы не наблюдалось, так как приложение имеет много неспешных анимаций и заметь проседание в производительности невозможно.

P.S.
Я знаю, что с iOS 9 все должно поменяться и apple будет предоставлять контент только нужный для данного устройства. Но, даже когда я удалил все 1x изображения — размер билда стал 135. При этом я пожертвовал памятью на неретиновых девайсах и качеством изображений, что заметно невооруженным глазом на некоторых элементах интерфейса.

P.S.S.
Раз тут речь зашла о памяти, то хотел бы поделится еще одним интересным фактом. Я всегда создаю изображения с
+ (UIImage *)imageNamed:(NSString *)name;

Быстрый и удобный способ, имеющий побочное действие. Когда пользователь отправляет приложение в background, система вполне может выкинуть картинку из памяти (а на iPad mini она это делает всегда), она ведь точно знает, что это изображение взято из Bundle, и оно наверняка еще на месте. Когда приложение вернется в foreground, система подгрузит их, как только они понадобятся. Звучит вполне логично и целесообразно. Однако, может вызывать лаги при открытии приложения из background, которых не было даже при холодном старте приложения. В моем случае это была карусель, и она лагала только когда приложение поднимали из background.

Всем хороших выходных и поменьше вот таких неприятных сюрпризов.

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


  1. InstaRobot
    05.07.2015 03:09

    А как насчет использования векторной графики в приложении?


    1. NikolayJuly Автор
      05.07.2015 03:39

      Векторная графика не всегда возможна. К примеру в сказках есть животные — это рисованные от руки персонажи, с тенями и там далее. Они не представимы в виде векторных изображений. Конечно есть изображения, которые, по сути, это фон + эффект и тогда я откажусь от картинки и буду в программно применять эффект. А если что то смогу свести к svg — то и вообще заиспользую сторонние либы. Но эта статья была про достаточно простое решение, которое сьело разумное время, зато дало хороший результат.


    1. SBKarr
      05.07.2015 09:11

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


  1. istui
    05.07.2015 11:14

    Очень хорошо, что вы оптимизируете приложения под mini 1gen. К сожалению, мини во многих случаях работает в «однозадачном» режиме: переключаешься из одного тяжелого приложения в другое — происходит перезапуск программы с ноля.


    1. NikolayJuly Автор
      05.07.2015 15:25

      Когда на iPad Mini я увидел 23Mb — я очень обрадовался. У приложения есть все шансы пережить ночь без использования на девайсе)


  1. greenkaktus
    05.07.2015 12:15
    +1

    pngquant хорош для PNG файлов.


    1. AnthonyBY
      05.07.2015 14:56

      да, я юзаю, ImageOptim, часто помогает обжать на 50% без заметной потери в качестве


  1. valeriyvan
    05.07.2015 12:52
    +5

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

    Это за гранью добра и зла.


    1. NikolayJuly Автор
      05.07.2015 15:23

      Решение и вправду спорное.
      Но это необходимо с точки зрения маркетинга. Зато приложение реже умирает в background даже на iPad Mini, то есть ли вы ненадолго отвлеклись во время чтения сказки, то и перезапуск приложения не произойдет, и логотип вы не увидите.
      Второй момент — на новый iPad вы даже не успеваете прочитать название компании.


      1. istui
        05.07.2015 18:22

        а возможно задавать разные задержки для разных устройств?


        1. NikolayJuly Автор
          05.07.2015 20:54

          конечно. никому не нужна задердка после 5 секунд старта. даже болше скажу, меряю паузу от попадания в main.


          1. valeriyvan
            06.07.2015 09:47

            До попадания в main уже может пройти до нескольких секунд в экстремальных случаях!


            1. NikolayJuly Автор
              06.07.2015 16:48

              Ну у нас не экстремальный случай. Несмотря на небольшой размер bundle, открывается быстро(бинарь сам по себе небольшой).


      1. valeriyvan
        06.07.2015 09:45

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

        Искусственная задержка при старте приложения вредит юзбилити! И если эта задержка нужна только чтоб показать логотип…


        1. NikolayJuly Автор
          06.07.2015 16:44

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


  1. Vavius
    05.07.2015 15:33
    +1

    Как замену png имеет смысл рассмотреть webp.
    Если lossless, то можно рассчитывать на 30% экономии по сравнению с png после ImageOptim.
    Lossy с качеством 90-100% на глаз не отличается, а размер в несколько раз меньше (сильно зависит от картинок, на одном проекте экономия была в 3 раза, на другом — в 7 раз).

    Конечно, webp далеко не самый лучший кодер по соотношению качество/размер. Но очень удобный по доступности и простоте интеграции библиотеки, плюс производительность не хуже чем у png.


  1. kostyl
    05.07.2015 18:50

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


    1. SBKarr
      05.07.2015 20:57
      +2

      Проводили как-то A/B тестирование по поводу постраничной подгрузки, правда, не книги, а журнала. В одном варианте грузили всё сразу, только потом давали открыть журнал. Во втором — читать можно сразу, каждая следующая страница подгружалась по мере просмотра. Контрольный параметр — среднее время нахождения пользователя в просмотрщике журнала при загруженной странице. Вариант с предварительной загрузкой победил. При тесте на контрольной группе, в живую, поведение варианта А: ткнул в загрузить, переключился в браузер/мессенжер/сходил за чаем/поговорил с соседом, получил уведомление, вернулся и залип в чтении. Вариант B: пользователь собирается сразу залипнуть в чтении, но загрузки каждой страницы приходится ждать, при этом время загрузки не позволяет сделать какое-то осмысленное действие. Быстро надоедает.
      Для чистоты добавили кеширование, приоритетную загрузку (приложение тянет сразу текущую страницу и последующие, не ждёт запрошенных до этого), результат стал лучше, но предварительная загрузка по прежнему выигрывала.
      Если ЦА — дети, то тут фактор «раздражает ждать» будет намного сильнее.


    1. NikolayJuly Автор
      05.07.2015 23:46
      +1

      Вариант грузить постранично даже не рассматривается. На странице одно предложение. И раз уж мы стараемся сделать хороший продукт и идем на встречу тем у кого 3g, то и книжка должна быть вся на девайсе когда пользователь ее читает.
      Вы конечно простите, но постраничная загрузка… это прям вэб. А наше приложение отлично работает и без инета(как минимум одна книжка в комплекте), а потом можно скачать остальные и спокойно отключится от всех сетей.
      Одно из главных преимуществ нативных приложений — это работа в офлайн. С ним может поспорить только «скорость работы».