Время запуска приложения

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

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

Различные состояния запуска приложения

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

Рекомендуем всегда оптимизировать приложение с учетом холодного запуска. Это также может улучшить производительность теплых и горячих запусков.

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

Два важных показателя, определяющих запуск приложения, – это время до начальной отрисовки (TTID) и время до полной отрисовки (TTFD). TTID – это время, необходимое для отображения первого кадра, а TTFD – время, необходимое для того, чтобы приложение стало полностью интерактивным. Оба показателя одинаково важны, поскольку TTID сообщает пользователю, что приложение загружается, а TTFD – что приложением уже можно пользоваться. Если задержка слишком велика, пользователь может закрыть приложение, не дождавшись его загрузки.

Холодный запуск

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

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

В начале холодного запуска система выполняет три задачи:

  1. Загрузите и запустите приложение.
  2. Показывать пустое окно сразу после запуска приложения.
  3. Создайте процесс приложения.

Как только система создает процесс приложения, он отвечает за следующие этапы:

  1. Создайте объект приложения.
  2. Запустите основной поток.
  3. Создайте основное действие.
  4. Инициализируйте интерфейс.
  5. Нарисуйте интерфейс на экране.

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

Дополнительную информацию о фазах макета в Compose можно найти в разделе Фазы Jetpack Compose.

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

Создание приложения

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

Если вы переопределите Application.onCreate в своем приложении, система вызовет метод onCreate для объекта приложения. Затем приложение порождает основной поток, также известный как поток UI, и поручает ему создать основное действие приложения.

С этого момента процессы на уровне системы и приложения выполняются в соответствии с этапами жизненного цикла приложения.

Создание действий

После того как процесс приложения создаст действие, оно выполнит следующие операции:

  1. Инициализирует значения.
  2. Вызывает конструкторы.
  3. Вызывает метод обратного вызова, например Activity.onCreate, соответствующий текущему состоянию жизненного цикла объекта activity.

Как правило, метод onCreate оказывает наибольшее влияние на время загрузки. В приложении Jetpack Compose задержка метода onCreate часто связана с начальной композицией. Это происходит, когда приложение вызывает setContent и запускает composable-функции верхнего уровня. Глубокая или сложная иерархия пользовательского интерфейса, а также composable-функции, выполняющие тяжелые вычисления в основном потоке, могут увеличить время компоновки.

Теплый запуск

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

  • Пользователь выходит из приложения, а затем снова запускает его. Процесс может продолжать выполняться, но приложение должно заново создать действие, используя вызов onCreate.

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

Горячий запуск

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

Однако если часть памяти была очищена в ответ на события сокращения памяти, например onTrimMemory, то эти объекты необходимо создать заново в ответ на событие горячего запуска.

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

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

Как определить запуск приложения в Perfetto

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

  1. В Perfetto найдите строку с производным показателем запуска приложений для Android. Если вы не видите его, попробуйте выполнить трассировку с помощью приложения для трассировки системы на устройстве.

    Рисунок 2. Фрагмент производного показателя запуска приложений Android в Perfetto.
  2. Нажмите на нужный фрагмент и нажмите клавишу M, чтобы выбрать его. Скобки вокруг фрагмента обозначают его длительность. Продолжительность также указана на вкладке Текущий выбор.

  3. Закрепите строку Android App Startups, нажав на значок булавки, который появляется при наведении указателя мыши на строку.

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

  5. Увеличьте масштаб основного потока, который обычно находится вверху, нажав W. Чтобы уменьшить масштаб, переместиться влево или вправо, нажмите S, A или D соответственно.

    Рисунок 3. Фрагмент производного показателя "Запуск приложения Android" рядом с основным потоком приложения.
  6. Фрагмент производных показателей позволяет легко увидеть, что именно включено в запуск приложения, чтобы вы могли продолжить отладку более подробно.

При анализе приложения Jetpack Compose вы можете использовать трассировку композиции, чтобы получить подробные сведения о производительности интерфейса. Обратите особое внимание на разделы трассировки в основном потоке, которые начинаются с Choreographer#doFrame. Обратите внимание на такие сегменты, как Compose:recompose, Compose:layout и Compose:draw. Они показывают, сколько времени ваше приложение тратит на создание, измерение, размещение и отрисовку определенных компонентов.

Как использовать показатели для анализа и улучшения запусков

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

Преимущества использования показателей запуска

Android использует показатели время до начальной отрисовки (TTID) и время до полной отрисовки (TTFD), чтобы оптимизировать холодный и теплый запуск приложения. Android Runtime (ART) использует данные этих показателей, чтобы эффективно предварительно компилировать код для оптимизации будущих запусков.

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

Время до начальной отрисовки

Время до начальной отрисовки (TTID) – это время, которое требуется для отображения первого кадра интерфейса приложения. Этот показатель отражает время, которое требуется приложению для создания первого кадра, включая инициализацию процесса при холодном запуске, создание действия при холодном или теплом запуске и показ первого кадра. Чем меньше TTID, тем быстрее запускается приложение и тем удобнее оно для пользователей. Фреймворк Android автоматически регистрирует TTID для каждого приложения. При оптимизации запуска приложения рекомендуем использовать reportFullyDrawn, чтобы получать информацию о TTFD.

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

  • Запуск процесса.
  • Инициализация объектов.
  • Создание и инициализация действия хоста.
  • Инициализация интерфейса.
  • Первая отрисовка приложения.

Получить TTID

Чтобы найти TTID, в инструменте командной строки Logcat найдите строку вывода, содержащую значение Displayed. Это значение и есть TTID. Оно выглядит примерно так, как в примере ниже, где TTID составляет 3 с 534 мс:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Чтобы найти TTID в Android Studio, отключите фильтры в представлении Logcat в раскрывающемся меню фильтров, а затем найдите время Displayed, как показано на рисунке 4. Отключение фильтров необходимо, поскольку этот журнал обслуживается системным сервером, а не самим приложением.

Рисунок 4. Отключенные фильтры и значение Displayed в Logcat.

Показатель Displayed в выходных данных Logcat не обязательно отражает время, необходимое для загрузки и отображения всех ресурсов. В него не входят ресурсы, на которые нет ссылок в исходной композиции (например, асинхронно загруженные данные или изображения), и ресурсы, созданные приложением в процессе инициализации объекта. Они не учитываются, поскольку загружаются в фоновом режиме и не блокируют показ приложения. Подробнее о ресурсах в Compose…

Иногда строка Displayed в выводе Logcat содержит дополнительное поле с общим временем, как в следующем примере:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

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

Мы рекомендуем использовать Logcat в Android Studio, но если вы не используете Android Studio, вы также можете измерить TTID, запустив приложение с помощью команды диспетчера действий оболочки adb. Рассмотрим пример.

adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

Показатель Displayed появится в выходных данных Logcat, как и раньше. В окне терминала появится следующее:

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

Аргументы -c и -a необязательны и позволяют указать <category> и <action>.

Время до полного показа

Время до полного отображения (TTFD) – это время, которое требуется приложению, чтобы стать интерактивным для пользователя. Это время, которое требуется для показа первого кадра интерфейса приложения, а также контента, который загружается асинхронно после показа первого кадра. Обычно это основной контент, загруженный из сети или с диска, о котором сообщает приложение. Другими словами, TTFD включает TTID, а также время, необходимое для того, чтобы приложение стало пригодным для использования. Чем меньше TTFD, тем удобнее приложение для пользователей, поскольку они могут быстрее начать с ним взаимодействовать.

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

Получить TTFD

Чтобы определить TTFD, сообщите о полной отрисовке, вызвав метод reportFullyDrawn класса ComponentActivity. Метод reportFullyDrawn сообщает, когда приложение полностью отрисовано и готово к использованию. TTFD – это время, прошедшее с момента, когда система получает намерение запустить приложение, до вызова reportFullyDrawn. Если вы не вызовете reportFullyDrawn, значение TTFD не будет передано.

Чтобы измерить TTFD, вызовите reportFullyDrawn после полной отрисовки интерфейса и всех данных. Не вызывайте reportFullyDrawn до того, как окно первого действия будет отрисовано и показано, поскольку в этом случае система сообщит время, измеренное ею. Другими словами, если вы вызовете reportFullyDrawn до того, как система определит TTID, она сообщит одинаковое значение для TTID и TTFD, и это значение будет значением TTID.

При использовании reportFullyDrawn Logcat показывает выходные данные, как в примере ниже, где TTFD составляет 1 с 54 мс:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

В выходных данных Logcat иногда указывается время total, как описано в разделе Время до первоначального показа.

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

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

Повысить точность времени запуска

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

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

Чтобы включить заполнение списка в контрольное время, получите FullyDrawnReporter с помощью fullyDrawnReporter и добавьте к нему регистратор в коде приложения. Освободите объект Reporter после того, как фоновая задача заполнит список.

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

Если в вашем приложении используется Jetpack Compose, вы можете указать состояние полной отрисовки с помощью следующих API:

  • ReportDrawn – указывает, что composable-функция готова к взаимодействию.
  • ReportDrawnWhen принимает предикат, например list.count > 0, чтобы указать, когда composable-функция готова к взаимодействию.
  • ReportDrawnAfter принимает приостанавливающий метод, который после завершения указывает, что composable-функция готова к взаимодействию.

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

class MainActivity : ComponentActivity() {

    sealed interface ActivityState {
        data object LOADING : ActivityState
        data object LOADED : ActivityState
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var activityState by remember {
                mutableStateOf(ActivityState.LOADING as ActivityState)
            }
            fullyDrawnReporter.addOnReportDrawnListener {
                activityState = ActivityState.LOADED
            }
            ReportFullyDrawnTheme {
                when(activityState) {
                    is ActivityState.LOADING -> {
                        // Display the loading UI.
                    }
                    is ActivityState.LOADED -> {
                        // Display the full UI.
                    }
                }
            }
            SideEffect {
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}
Выявление узких мест

Чтобы найти узкие места, можно использовать Профилировщик ЦП Android Studio. Подробнее о том, как проверять активность ЦП с помощью Профилировщика ЦП…

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

Решение распространенных проблем

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

Тяжелая инициализация приложения

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

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

Другие проблемы при инициализации приложения включают события сборки мусора, которые являются эффективными или многочисленными, или дисковый ввод-вывод, происходящий одновременно с инициализацией, что дополнительно блокирует процесс инициализации. Сборка мусора особенно важна для среды выполнения Dalvik. Среда Android Runtime (ART) выполняет сборку мусора параллельно, сводя к минимуму влияние этой операции.

Диагностика проблемы

Чтобы выявить проблему, можно использовать трассировку методов или встроенную трассировку.

Трассировка методов

Запустив Профилировщик ЦП, вы увидите, что метод callApplicationOnCreate в конечном итоге вызывает ваш метод com.example.customApplication.onCreate. Если инструмент показывает, что выполнение этих методов занимает много времени, изучите подробнее, что именно там происходит.

Встроенная трассировка

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

  • Исходная функция onCreate вашего приложения.
  • Любые глобальные объекты-одиночки, которые инициализирует ваше приложение.
  • Любые операции ввода-вывода на диске, десериализация или узкие циклы, которые могут происходить во время узкого места.

Решения проблемы

Независимо от того, связана ли проблема с ненужными инициализациями или с дисковым I/O, решением является отложенная инициализация. Другими словами, инициализируйте только те объекты, которые нужны немедленно. Вместо создания глобальных статических объектов перейдите к шаблону Singleton, в котором приложение инициализирует объекты только тогда, когда они нужны в первый раз.

Также рекомендуем использовать фреймворк внедрения зависимостей, например Hilt, который создает объекты и зависимости при их первом внедрении.

Если ваше приложение использует поставщиков контента для инициализации компонентов при запуске, рекомендуем вместо этого использовать библиотеку App Startup.

Инициализация ресурсоемких действий

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

  • Инициализация большого или сложного интерфейса.
  • Тяжелая инициализация в composable-функциях.
  • Блокировка отрисовки экрана на диске или сетевой ввод-вывод.
  • Загрузка и декодирование растровых изображений.
  • Растеризация объектов VectorDrawable.
  • Инициализация других подсистем объекта activity, в котором размещено приложение.

Диагностика проблемы

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

Трассировка методов

При использовании Профилировщика ЦП обратите внимание на конструкторы подклассов Application и методы com.example.customApplication.onCreate.

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

Встроенная трассировка

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

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

Решения проблемы

Ниже описаны две распространенные проблемы и способы их решения.

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

Подробнее о рекомпозиции рассказывается в статьях Recomposition и Get recomposition counts.

Собственные заставки

Если вы ранее использовали один из следующих методов для реализации собственной заставки в Android 11 (уровень API 30) или более ранней версии, то можете заметить, что запуск приложения стал занимать больше времени:

  • Используйте атрибут темы windowDisablePreview, чтобы отключить пустой экран, который система показывает при запуске.
  • Используется выделенный Activity.

Начиная с Android 12, необходимо перейти на API SplashScreen. Этот API позволяет ускорить запуск и настроить заставку следующими способами:

Кроме того, библиотека совместимости поддерживает обратную совместимость с SplashScreen API и позволяет создавать заставки, которые будут одинаково выглядеть во всех версиях Android.

Подробную информацию об этом вы найдете в руководстве по переносу заставок.