Время запуска — самый важный показатель, который вы никогда не оптимизируете. Представьте, что вы — WhatsApp. У вас невероятное количество ежедневно активных пользователей — кроме США, по какой-то причине. Каждый из них может выполнять тёплый запуск приложения по десять раз в день. А может, и чаще.
Если запуск занимает 0,5 секунды, умножить это на 10, а затем ещё на невероятное количество пользователей, то даже улучшение на 1%, вероятно, сэкономит несколько человеческих жизней. Я не проверял расчёты и не собираюсь.
К счастью, Apple свои расчёты проверила. На сессии WWDC 2019 года «Optimizing App Launch» компания подсчитала, что при миллиардах ежедневных запусков сокращение времени запуска всего на одну миллисекунду ежедневно экономит примерно столько времени, сколько нужно, чтобы отправить ракету на Марс.
В этом году на WWDC Apple устроила своего рода «год Snow Leopard». Меньше эффектных функций, меньше ненужных переделок интерфейса и больше незаметных системных оптимизаций. Компания заявила, что в iOS 27 скорость запуска приложений выросла на 30%.
Раньше я уже рассказывал об оптимизации времени запуска старого приложения и отслеживании производительности запуска в продакшене. Но до самых низких уровней мы ещё не спускались. Сегодня именно этим и займёмся.
Мы:
- разберём основные вещи, которые разработчику нужно знать о времени запуска;
- исследуем улучшения iOS 26 и iOS 27 с помощью Instruments;
- выясним, что именно Apple действительно оптимизировала.
Теория запуска
Виды запуска
Существует три вида запуска приложения.
🐢 Холодный запуск
Холодный запуск — это то, что обычно подразумевается под словом «запуск». Процесс приложения ещё не существует, а исполняемый файл находится в постоянном хранилище. На WWDC 2016 объясняли, что настоящий холодный запуск в основном происходит только при первом запуске после установки приложения или перезагрузки устройства. Однако именно он создаёт первое впечатление, поэтому о его скорости стоит беспокоиться.
🐇 Тёплый запуск
При тёплом запуске iOS создаёт новый процесс, однако страницы памяти с исполняемым кодом уже активно кэшируются ядром в файловом кэше. Кроме того, dyld сохраняет заранее рассчитанный план запуска. Проще говоря, необходимые данные уже находятся ближе к месту, где они понадобятся.
🐅 Горячего запуска не существует
Так называемый горячий запуск — это обычное возобновление работы. Приложение, находившееся в фоне или приостановленном состоянии, возвращается на передний план. Его процесс уже существует, а данные находятся в оперативной памяти.
Этапы запуска
Процесс запуска можно разделить на две фазы в зависимости от того, когда в процессе приложения вызывается функция main(). Понимание этапов, происходящих до main, может помочь на собеседовании, особенно если интервьюер любит задавать каверзные вопросы.
Сначала ядро отображает исполняемый файл Mach-O в память процесса. Затем динамический компоновщик dyld загружает динамические фреймворки, необходимые приложению. Среди них собственный код приложения, сторонние зависимости, системные фреймворки Foundation, UIKit и Swift.
Address Space Layout Randomization, или ASLR, является важным механизмом защиты от эксплуатации ошибок памяти. При каждом запуске он случайным образом меняет адреса многих фреймворков в памяти процесса. Поэтому dyld выполняет так называемые fixups — заменяет ссылки на символы реальными адресами памяти.
После этого среда выполнения Swift настраивает метаданные типов, соответствия протоколам, обобщённые типы. Наконец, может выполниться часть вашего собственного кода: статические инициализаторы и методы Objective-C load().
Это огромный объём работы, и вскоре мы увидим все эти процессы в трассировках Instruments.
За знание этапов после main работу вам, скорее всего, не дадут. Если вы увлекаетесь приложениями командной строки или просто любите полный контроль, у вас может быть собственная явная функция main():
// main.swift
import UIKit
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
nil,
NSStringFromClass(AppDelegate.self)
)
UIApplicationMain создаёт объект приложения, настраивает цикл запуска, делегат приложения и в итоге вызывает application(_:willFinishLaunchingWithOptions:). Остальную последовательность вы, вероятно, знаете. А если нет, на моём сайте есть много материалов про разработку под iOS.
Запуск заканчивается, когда отображается первый кадр интерфейса. Оптимизация этой части — обычная инженерная работа: профилирование, выполнение задач параллельно, минимизация объёма работы на критическом пути до момента, когда пользователь увидит нужный ему интерфейс.
Чтение трассировки запуска
Инструмент App Launch предоставляет удобное визуальное представление описанной выше теории.
- Этап
Launch Executableохватывает весь процесс до вызоваmain. Validating Closureв начале трассировки указывает на тёплый запуск. На этом этапе загружается план запуска, заранее рассчитанный dyld. При холодном запуске вместо него отображаетсяBuilding Closure. Этот этап находится ближе к концу и занимает значительно больше времени.Map Imageполучает реальные пути к файлам динамических фреймворков и отображает их в память процесса черезmmap. Именно здесь чрезмерное количество динамических фреймворков может замедлить запуск.Apply Fixupsсвязывает ссылки на символы с реальными адресами памяти.ObjC Image Initделает классы доступными среде выполнения Objective-C.Run Static Initializerзапускает статические инициализаторы.dlopenотвечает за загрузку библиотек во время выполнения. Вы даже можете вызывать эту функцию самостоятельно.
После main в трассировке также можно увидеть инициализацию UIKit, вызов didFinishLaunchingWithOptions, создание UIScene, итоговый переход в состояние Foreground — Active.
В Xcode 27 появился удобный flame graph.
В инструменте Time Profiler нажмите маленькую синюю кнопку в правом верхнем углу, выберите процесс приложения, затем главный поток. После этого появится график выполнения.
Процентное значение показывает долю процессорного времени каждого участка в выбранном временном диапазоне. Чтобы получить более полезную картину, можно скрыть системные библиотеки и посмотреть, как распределяется время запуска именно в вашем коде.
Flame graph полезен не только для анализа запуска, но и для других задач оптимизации производительности.
У меня есть только один шанс 🎯
Я не Apple. У меня нет склада дешёвых iPhone 11 Pro Max, на которых можно проводить профилирование. У меня есть только одна возможность в год выполнить полноценное сравнение производительности до и после обновления операционной системы.
Чтобы результаты не получились слишком хорошими, я достал свой надёжный iPhone 13 mini из ящика с кабелями, которые по непонятной причине разбросаны по всему столу.
К счастью, я вспомнил об эксперименте прямо перед тем, как автоматически установилась бета-версия системы. Едва успел.
Я быстро навайбкодил намеренно ужасное приложение-монстра из динамических фреймворков, статических инициализаторов, соответствий протоколам, демонстрационных приложений из предыдущих статей. Я решил, что результаты будут интереснее, если попытаться вызвать у dyld несварение.
Хорошо. Вот часть, которую вы ждали. Посмотрим, что изменилось в iOS 27.
iOS 26 против iOS 27
Поскольку я очень глупый, на iOS 26 я сделал только три трассировки — два тёплых запуска и один холодный. После этого на iOS 27 я уже сделал два тёплых запуска и два холодных.
Я не настоящий учёный, а вы в основном читаете это ради хорошего настроения. При необходимости можете провести профилирование самостоятельно.
Код и трассировки эксперимента опубликованы в открытом доступе.
Холодный запуск в iOS 26
Общее время: 1090 мс
Из них:
- pre-main — около 550 мс;
- post-main — около 540 мс.
Тёплый запуск в iOS 26
Общее время: 582 мс
Из них:
- pre-main — около 145 мс;
- post-main — около 437 мс.
Сразу заметно, насколько сильно сократилось время pre-main. Этот этап стал почти в четыре раза быстрее. Особенно значительно ускорились загрузка замыканий и отображение памяти. Синий этап fixups занимает примерно столько же времени. Это логично, поскольку ASLR применяется при каждом запуске.
Хорошо. Давайте пойдем дальше.
Необратимо устанавливает iOS 27.
Холодный запуск в iOS 27
Общее время: 862 мс
Из них:
- pre-main — около 470 мс;
- post-main — около 390 мс.
Тёплый запуск в iOS 27
Общее время: 449 мс
Из них:
- pre-main — около 67 мс;
- post-main — около 382 мс.
В нашем не слишком научном тесте удалось подтвердить ускорение, хотя результаты и не являются статистически значимыми.
- Холодный запуск стал быстрее на 21%.
- Тёплый запуск стал быстрее на 23%.
Когда то, что вы пытаетесь проверить, на самом деле близко к заявленным Apple 30%.
Большая часть улучшения пришлась на фазу pre-main. Измерения показали — перестроение closure ускорилось примерно в 1,5 раза, fixups — в 2,5 раза, статические инициализаторы — в 3 раза. Оставшаяся часть pre-main в основном состоит из операций ввода-вывода, например отображения в память динамических исполняемых файлов. Эти этапы на обеих версиях системы выглядят примерно одинаково.
Форма фазы dyld также осталась практически неизменной. Это не полная переработка dyld, которую Apple выполняла в предыдущих версиях системы. Однако результаты согласуются с заявлениями компании. Apple утверждает, что ускорение на 30% обеспечили две технологии:
- Первая — «оптимизированный планировщик процессора». Подозреваю, что на языке маркетинга это означает более эффективное параллельное выполнение задач dyld.
- Вторая — «предварительная загрузка ключевых данных приложения». Вероятно, это связано с ускоренным построением замыканий и более быстрым выполнением статических инициализаторов, которые мы увидели в измерениях.
Этап post-main также немного ускорил отображение первого кадра, когда Core Animation отправляет интерфейс на отрисовку. Большинство остальных этапов запуска в моём тестовом проекте представляют собой глупые вызовы sleep(), которыми я имитировал плохо спроектированную последовательность запуска.
Как Apple улучшала запуск приложений
За последние десять с лишним лет Apple неоднократно рассказывала об ускорении запуска.
2017 год: dyld3
Apple полностью переписала динамический компоновщик и сначала использовала его в собственных приложениях.
Разбор Mach-O и разрешение символов были вынесены в отдельный процесс, а результаты кэшировались в виде launch closures.
2018 год: запуск до двух раз быстрее
Apple заявила, что приложения могут запускаться до двух раз быстрее при активной многозадачности. Система заранее повышала частоту процессора, когда определяла, что пользователь прокручивает домашний экран и, вероятно, собирается открыть приложение.
2019 год: ещё одно ускорение до двух раз
Apple распространила dyld3 на сторонние приложения и снова заявила, что запуск может стать до двух раз быстрее.
2022 год: некоторые приложения запускаются почти вдвое быстрее
Ускорение обеспечили улучшения динамического компоновщика. Среди них — заранее рассчитанные соответствия протоколам, page-in linking, отложенное выполнение fixups при обращении к странице памяти вместо выполнения всех операций во время запуска.
2026 год: запуск до 30% быстрее
Apple объясняет ускорение оптимизированным планировщиком процессора и предварительной загрузкой ключевых данных приложения. Я не буду снова это разбирать. Также я нашёл интересный материал CodeColorist с анализом декомпилированного кода, в котором рассматриваются возможные оптимизации кэша dyld.
Хм.
Ускорение в два раза, затем ещё в два, ещё в два и потом в 1,3 раза. В сумме получается потенциальное ускорение запуска некоторых приложений примерно в 10,4 раза.
Но подождите.
С 2016 года мы также перешли от процессоров A9 к A19. Это ещё примерно десятикратный прирост. Тогда почему запуск приложений не стал примерно в сто раз быстрее, чем в 2016 году?
Думаю, нам просто нужно выполнять меньше всякой ерунды во время запуска.
Давайте отключим хотя бы часть этих маркетинговых SDK 👀
Последний заказ
Каждые несколько лет Apple выводит системных инженеров из спячки, чтобы они немного улучшили динамический компоновщик, планирование процессора или предварительную загрузку данных.
В результате компании удаётся выжать из времени запуска очередное двукратное ускорение, постепенно уменьшающиеся до 1,3 раза или, что более вероятно, зафиксированные мной 1,2 раза.
Это хорошо. Однако ответственность за архитектуру критического пути запуска по-прежнему лежит на разработчике.
- Профилируйте запуск с помощью инструмента App Launch и flame graph.
- Оптимизируйте pre-main, осознанно используя статические библиотеки, динамические библиотеки, mergeable libraries.
- Минимизируйте объём работы на критическом пути post-main.
- Как можно быстрее отображайте начальный интерфейс, показывая только то, что действительно нужно пользователю.
На этом с запуском в iOS 27 всё. Увидимся в следующем году 👋

