Опубликовано по ссылке: http://www.highload.ru/2016/abstracts/2328.html

Быстрый старт iOS приложения на примере iOS Почты Mail.Ru

Тезисы

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

- Что можно и нужно оптимизировать?
- Как сократить время от нажатия на иконку до показа экрана запуска?
- Инструменты анализа производительности: не только Time Profiler.
- Что быстрее: XIB или создание UI в коде?
- Замеры скорости запуска как часть Continuous Integration.

Видео

Слайды

Текст выступления

Слайд-заголовок

Всем привет! Меня зовут Николай Морев, и я разрабатываю приложение Почты Mail.Ru для iOS. Для тех, кто никогда о нем не слышал, несколько фактов:

О нашем приложении

Почта Mail.Ru для iOS

Пролистываем отзывы

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

Проблема медленного запуска

Много ли в зале iOS разработчиков?

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

Пролистываем отзывы

Наши пользователи вообще очень внимательные и время от времени напоминают нам о том, что для них действительно важно.

Старт быстрее N секунд

К тому же данные аналитики подтверждали наличие проблемы: график.

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

Кстати, мой коллега Даниил Румянцев недавно делал доклад на встрече CocoaHeads про определение качества сетевого соединения. К сожалению, видео с этой встречи не выложено в открытый доступ, поэтому, если вам будет интересно, обращайтесь, помогу найти информацию.

Актуальность

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

Частое использование

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

Исторически сложилось

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

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

Отсутствие контроля

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

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

Profiling (график)

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

Прежде чем начать

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

Всем известно главное правило оптимизации: преждевременная оптимизация - корень всех зол. Поэтому прежде чем начать, необходимо определиться с тем,

Сценарий

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

Эффект

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

Как измерить?

Для поиска мест, которые можно оптимизировать, мы использовали Time Profiler, а для оценки эффекта от оптимизации мы использовали логи времени выполнения, встроенные в приложение.

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

Предел оптимизации

Ну и, наконец, чтобы проверить, до какой степени мы потенциально можем уменьшить скорость запуска, понять теоретический предел улучшений и решить, есть ли вообще смысл заморачиваться, мы создали простое тестовое приложение, реализующее самым простым способом ту минимальную функциональность, от которой никак невозможно отказаться:

Первый этап

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

Слайд с графиком из TP

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

WWDC

Очень подробный рассказ про первый этап был на WWDC в этом году в замечательном докладе 406 Optimizing App Startup Time. Там подробно рассказано обо всем, что происходит на этом этапе и что мы можем с этим сделать. Перескажу очень коротко основные моменты из него.

Что происходит на этом этапе? iOS загружает исполняемый код приложения в память, производит над ним необходиые манипуляции: сдвиг указателей, привязка указателей, ссылающихся на внешние библиотеки, проверка подписи всех исполняемых файлов. Затем выполняются методы +load и статические конструкторы.

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

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

Do less stuff

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

Рекомендации

Второй этап

Это все классно, но хотелось бы уже перейти к оптимизации собственного кода, где у нас больше простор для действий. Естественно мы начали с исследования с помощью Time Profiler-а. Для тех, кто не знает, это инструмент, снимающий с работающего приложения стек трейсы каждую миллисекунду, по которым затем строится дерево вызовов и видна длительность каждого вызова. Он показывает не только код, который вы сами написали, но и всё, что лежит ниже - на уровне системных фреймворков.

Проблемы

Time Profiler - это очень крутой и мощный инструмент, но он не решает все ваши проблемы автоматически. Вот с какими сложностями мы столкнулись:

Не вся информация доступна

Провалы

Разброс

Советы

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

Что мы сделали ленивым:

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

Слайд про nib

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

Ввод-вывод

В принципе чтение/запись на флэш-память происходит на современных девайсах очень быстро - единицы или десятки миллисекунд, поэтому не всегда стоит над этим заморачиваться, но бывает так, что ваш или сторонний код этим злоупотребляет, открывая слишком много файлов на старте. Например, мы обнаружили такую проблему с фреймворком аналитики Flurry и раскиданными по всему коду вызовами UIImage imageNamed.

Time Profiler не покажет такие места, они будут видны в нем в лучшем случае как небольшие провалы на графике CPU. Вместо этого можно использовать другой инструмент - I/O Activity, который показывает все системные вызовы связанные с вводом-выводом и имена файлов. Аналогичную информацию можно получить и просто с помощью отладчика и брейкпоинта на функцию __open.

Случай с системными фреймворками и XPC иногда можно отследить, обращая внимание на провалы на графике и предшествующие им вызовы в списке Call Stack-ов в профайлере.

Когда TP не дает достаточно информации: layout

Получить более подробную информацию по прогонам layout-а вам поможет свизлинг методов layoutSubviews во всех классах.

Пояснение для не iOS-разработчиков: свизлинг - это подмена имплементации метода, кое-что, что динамическая сущность Objective-C очень легко позволяет делать даже с системными методами.

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

Профайлинг логами

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

Использовать Time Profiler для этого - не вариант, так как сложно автоматизировать его запуски и программно собирать результаты выполнения, да и не нужен такой объем информации для этой задачи. Поэтому мы в само приложение добавили код, выводящий в консоль и в файл профайлинговые логи.

Логи

Логи выглядят примерно так. Мы выбрали ключевые точки критического пути запуска приложения и расставили в них вызовы, которые выводят абсолютное время от самого первого вызова (+load) и относительное время от предыдущего вызова.

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

Примеры диаграмм

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

Автоматизация замеров времени старта

В сообществе разработчиков очень много говорится о Continuous Integration, TDD и других практиках непрерывного контроля качества кода, но почему-то очень мало информации о том, как постоянно контроллировать производительность. Мы попытались восполнить этот пробел.

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

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

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

Реализация

Расскажу о технической реализации:

Инструменты

Проблемы

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

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

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

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

Заключение

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

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

Видео

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

Результаты

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

Есть еще красивая картинка, сделанная нашими аналитиками по собираемой с пользователей стистике, которая показывает, что количество запусков приложения, попадающих в интервал от 0 до 2 секунд, увеличилось в 10 раз до половины всех запусков.

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

Удовлетворенность

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

Подробности

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

Спасибо

Время на вопросы 5-10 минут. Мой twitter и github.

Обновлено Tue Jun 18 23:54:28 2019 +0300