Site icon AppTractor

Как мы сократили время запуска нашего iOS-приложения на 60%

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

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

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

Изменили String(describing:) на ObjectIdentifier()

В начале 2022 года наш путь в оптимизации запуска приложений начался с визуализации основных узких мест с помощью инструмента Performance Analysis от Emerge Tools.

Рис. 1. Трассировка стека, показывающая три возможности оптимизации производительности

Этот инструмент смог показать неоптимизированные ветки как “с высоты птичьего полета”, так и с со всеми подробностями. Одной из самых заметных проблем было время, которое мы тратили на проверки соответствия протоколу Swift ( Swift protocol conformance checks), но почему?

Архитектурные принципы, такие как принцип единой ответственности, разделение задач и другие, являются ключевыми в том, как мы пишем код в DoorDash. Службы и зависимости часто внедряются и описываются по их типу. Проблема в том, что мы использовали String(describeing:) для идентификации служб, что привело к снижению производительности во время выполнения из-за проверки того, соответствует ли тип различным другим протоколам. Эта трассировка стека взята непосредственно из запуска нашего приложения, чтобы продемонстрировать это.

Рисунок 2. Трассировка стека того, что происходит за кулисами String(describing:) API

Первый вопрос, который мы задали себе — «Действительно ли нам нужна строка для идентификации типа?» Устранение требования к строке и переход на идентификацию типов с использованием вместо этого ObjectIdentifier, который является простым указателем на тип, позволил сократить время запуска приложения на 11%. Мы также применили эту технику к другим областям, где вместо необработанной строки было достаточно указателя, что дало дополнительное улучшение еще на 11%.

Если возможно использовать необработанный указатель на тип вместо использования String(describeing:), мы рекомендуем внести такое же изменение, чтобы сэкономить время.

Прекратили преобразовывать ненужные объекты в AnyHashable

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

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

Мы могли бы оптимизировать нашу стратегию хеширования несколькими способами, но мы начали с переосмысления наших первоначальных ограничений и границ. Первоначально хеш-значение команды представляло собой комбинацию связанных с ней элементов. Это решение было принято намеренно, поскольку мы хотели сохранить гибкую и мощную абстракцию команд. Но после повсеместного внедрения новой архитектуры мы заметили, что выбор такого дизайна был преждевременным и в целом остался неиспользованным. Изменение идентификации команд по их типу привело к ускорению запуска приложений на 29%, ускорению выполнения команд на 55% и ускорению регистрации команд на 20%.

Проверили инициализации сторонних фреймворков

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

Недавний аудит показал, что из-за определенного стороннего фреймворка наше iOS-приложение запускается примерно на 200 мс медленнее. Один только этот фреймворк занимал примерно 40% (!) времени запуска нашего приложения, как показано на рисунке.

Рис. 3. Диаграмма, показывающая, что примерно 200 мс из времени запуска нашего приложения тратилось на то, что сторонний фреймворк итерировал наш NSBundle.

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

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

Когда приложение запускается и загружается в память, за его подготовку отвечает динамический компоновщик (dyld). Одним из шагов dyld является сканирование динамически связанных фреймворков и вызов любых функций инициализации модулей, которые могут быть у него. Компоновщик делает это, ища разделы, помеченные типом 0x9 (S_MOD_INIT_FUNC_POINTERS), обычно расположенные в сегменте «__DATA».

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

Рассматриваемый сторонний фреймворк имел в общей сложности девять инициализаторов модулей, которые все из-за dyld запускались до того, как наше приложение запускало main(). Эти девять инициализаторов влияли на общее время и задерживали запуск нашего приложения. Итак, как мы это исправим?

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

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

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

Заключение

Точное определение узких мест и возможностей улучшения производительности часто являются самой сложной частью любой оптимизации. Известно, что распространенной ошибкой является измерение A, оптимизация B и формирование выводов C. Именно здесь хорошие инструменты повышения производительности помогают выявить узкие места и исправить их. Инструменты Xcode поставляются с несколькими шаблонами, помогающими выявить различные потенциальные проблемы в macOS/iOS-приложении. Но инструменты Emerge Tools могут более просто предоставить более точные данные о производительности приложений.

Источник

Exit mobile version