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

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

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

Разберитесь в различных состояниях запуска приложения.

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

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

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

Два важных показателя для определения запуска приложения — это время до первого отображения (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 App Startups в Perfetto.
  2. Щелкните по соответствующему сегменту и нажмите клавишу m , чтобы выбрать сегмент. Вокруг сегмента появятся скобки, указывающие на продолжительность его выполнения. Продолжительность также отображается на вкладке «Текущий выбор» .

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

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

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

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

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

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

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

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

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

Основные параметры Android

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

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

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

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

Время до первого отображения (TTID) — это время, необходимое для отображения первого кадра пользовательского интерфейса приложения. Этот показатель измеряет время, необходимое приложению для создания первого кадра, включая инициализацию процесса при холодном старте, создание активности при холодном или теплом старте и отображение первого кадра. Поддержание низкого значения TTID вашего приложения помогает улучшить пользовательский опыт, позволяя пользователям видеть быстрый запуск вашего приложения. TTID автоматически сообщается для каждого приложения фреймворком Android. При оптимизации для запуска приложения мы рекомендуем использовать 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 shell activity manager . Вот пример:

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

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

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() и добавьте к нему репортер в код вашего приложения. Освободите репортер после того, как фоновая задача завершит заполнение списка.

Метод reportFullyDrawn() класса FullyDrawnReporter вызывается только после того, как все добавленные репортеры будут освобождены. Добавление репортера до завершения фонового процесса позволяет включить в данные о времени запуска также время, необходимое для заполнения списка. Это не меняет поведение приложения для пользователя, но позволяет включить в данные о времени запуска время, необходимое для заполнения списка. 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 {
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Java

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. Дополнительную информацию см. в разделе «Проверка активности ЦП с помощью профилировщика ЦП» .

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

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

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

Интенсивная инициализация приложения

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}