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

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

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

Понимание различных состояний запуска приложения

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

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

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

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

Холодный старт

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

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

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

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

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

  1. Создайте объект приложения.
  2. Запускаем основной поток.
  3. Создайте основную деятельность.
  4. Раздувание просмотров.
  5. Разметка экрана.
  6. Выполните первоначальный розыгрыш.

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

На рис. 1 показано, как процессы системы и приложения передают друг другу работу.

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

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

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

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

Если вы переопределите Application.onCreate() в своем собственном приложении, система вызовет метод onCreate() для вашего объекта приложения. После этого приложение создает основной поток, также известный как поток пользовательского интерфейса , и поручает ему создать ваше основное действие.

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

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

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

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

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

Теплый старт

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

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

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

Горячий старт

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

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

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

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

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

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

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

    Рис. 3. Полученный срез показателей стартапов Android-приложений в Perfetto.
  2. Щелкните связанный фрагмент и нажмите m , чтобы выбрать его. Вокруг фрагмента появляются скобки, которые обозначают, сколько времени это заняло. Продолжительность также отображается на вкладке «Текущий выбор» .

  3. Закрепите строку «Запуски приложений Android», щелкнув значок булавки, который отображается, когда вы наводите указатель на строку.

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

  5. Увеличьте масштаб основной цепочки, обычно вверху, нажав w (нажимайте s, a, d, чтобы уменьшить масштаб, переместиться влево и вправо соответственно).

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

Используйте метрики для проверки и улучшения стартапов

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

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

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

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

Android Vitals

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

Android Vitals считает чрезмерным следующее время запуска вашего приложения:

  • Холодный запуск занимает 5 секунд или дольше.
  • Теплый запуск занимает 2 секунды или дольше.
  • Горячий запуск занимает 1,5 секунды или дольше.

Android Vitals использует показатель времени до первого отображения (TTID) . Информацию о том, как Google Play собирает данные Android Vitals, можно найти в документации Play Console .

Время до первого отображения

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

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

  • Запускаем процесс.
  • Инициализация объектов.
  • Создание и инициализация активности.
  • Раздувание макета.
  • Рисуем приложение впервые.

Получить TTID

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

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

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

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

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

Иногда строка 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, когда Choreographer вызывает метод onDraw() активности, и когда она знает, что вызывает его в первый раз. Однако система не знает, когда определить 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 до тех пор, пока ваше приложение не станет пригодным для использования, чтобы вы могли включить список. населения как часть вашего контрольного времени.

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

Чтобы включить заполнение списка в процесс измерения времени тестирования, получите FullyDrawnReporter с помощью getFullyDrawnReporter() и добавьте к нему репортер в код вашего приложения. Отпустите генератор отчетов после того, как фоновая задача завершит заполнение списка.

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

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

Котлин

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 {
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Ява

public class MainActivity extends ComponentActivity {
    private FullyDrawnReporter fullyDrawnReporter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        fullyDrawnReporter = getFullyDrawnReporter();
        fullyDrawnReporter.addOnReportDrawnListener(() -> {
            // Trigger the UI update.
            return Unit.INSTANCE;
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

               fullyDrawnReporter.removeReporter();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

                fullyDrawnReporter.removeReporter();
            }
        }).start();
    }
}

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

  • ReportDrawn : указывает, что ваш составной объект немедленно готов к взаимодействию.
  • ReportDrawnWhen : принимает предикат, например list.count > 0 , чтобы указать, когда ваш составной объект готов к взаимодействию.
  • ReportDrawnAfter : принимает метод приостановки, который после завершения указывает, что ваш составной объект готов к взаимодействию.
Выявление узких мест

Для поиска узких мест вы можете использовать профилировщик процессора Android Studio. Дополнительные сведения см. в разделе Проверка активности ЦП с помощью CPU Profiler .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Инициализация тяжелой активности

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

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

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

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

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

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

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

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

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

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

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

Существует множество потенциальных узких мест, но есть две распространенные проблемы и способы их устранения:

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

Пользовательские заставки

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

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

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

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

Подробности см. в руководстве по миграции экрана-заставки .

{% дословно %} {% дословно %} {% дословно %} {% дословно %}