
Мои предыдущие статьи были посвящены мониторингу запуска Android-приложений в эксплуатационной среде. После того, как мы разобрались с метриками и сценариями, которые результируют в медленном запуске приложения, следующим шагом будет повышение производительности.
Чтобы понять, почему приложение медленно запускается, нам нужно его профилировать. Android Studio предоставляет несколько типов конфигураций записи профилирования:
Трассировка системных вызовов (systrace, perfetto): почти не влияет на время выполнения, отлично подходит для понимания того, как приложение взаимодействует с системой и процессорами, но не вызовы методов Java, которые происходят внутри виртуальной машины приложения.
Сэмплирование функций C/C++ (Simpleperf): не особо меня заинтересовал, приложения, с которыми я работаю, выполняют гораздо больше байт-кода, чем нативного. На Q+ он также должен с минимальными накладными расходами сэмплировать Java-стеки, но мне так и не удалось это реализовать.
Трассировка методов Java: захватывает все вызовы методов виртуальной машины, что приводит к таким большим накладным расходам, что результаты мало что могут показать.
Сэмплирование методов Java: меньше накладных расходов, чем при трассировке, но показывает вызовы методов Java, происходящие внутри виртуальной машины. При профилировании запуска приложения это мой предпочтительный вариант.

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

Такая возможность все-таки существует, но она скрывается в конфигурации запуска вашего приложения: поставьте галочку возле “Start this recording on startup” на вкладке profiling .

Затем запустите приложение через Run > Profile app.

Профилирование релизных билдов
Android-разработчики в своей повседневной работе обычно используют дебажные билды, а дебажные билды зачастую включают в себя debug drawer, дополнительные библиотеки, такие как LeakCanary и т. д. Разработчикам следует профилировать релизные билды, а не дебажные, чтобы гарантировать, что они работают над актуальными проблемами, с которыми сталкиваются их клиенты.
К сожалению, релизные билды не подлежат отладке, поэтому профилировщик Android не может записывать трейсы релизных билдах.
Есть несколько вариантов решения этой проблемы.
1. Создание отлаживаемого релизного билда
Мы могли бы временно сделать наш релизный билд отлаживаемым или создать новый тип релизного билда только для профилирования.
android {
buildTypes {
release {
debuggable true
// ...
}
}
}Библиотеки и код Android Framework часто ведут себя иначе, если APK-файл является отлаживаемым. ART отключает множество оптимизаций для подключения дебагера, что значительно и порой непредсказуемо влияет на производительность (смотрите этот доклад). Так что это решение не идеальное.
2. Профилирование на рутованном устройстве
Рутованные устройства позволяют профилировщику Android Studio записывать трейсы на недебажных билдах.
Не рекомендую профилировать на эмуляторе - производительность каждого компонента системы будет отличаться (скорость процессора, размер кэша, дисковая производительность), “оптимизация” может даже замедлить работу, переключив работу на что-то, что медленнее телефоне. Если у вас нет рутованого устройства, вы можете создать эмулятор без Play Services, а затем запустить adb root.
3. Использование simpleperf на Android Q
Есть такой инструмент под названием simpleperf, который предположительно позволяет профилировать релизные билды на нерутованных устройства Q+, если у них есть специальный флаг в манифесте. В документации это называется profileableFromShell, в примере XML есть тег profileable с атрибутом android: shell, но в официальной документация по манифесту ничего про это не сказано.
<manifest ...>
<application ...>
<profileable android:shell="true" />
</application>
</manifest>Я просмотрел код парсинга манифеста на cs.android.com:
if (tagName.equals("profileable")) {
sa = res.obtainAttributes(
parser,
R.styleable.AndroidManifestProfileable
);
if (sa.getBoolean(
R.styleable.AndroidManifestProfileable_shell,
false
)) {
ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL;
}
}Похоже, вы можете запустить профилирование из командной строки, если в манифесте есть <profileable android:shell = "true" /> (сам я не пробовал). Насколько я понимаю, команда Android Studio все еще работает над интеграцией этой возможности.
Профилирование загруженного APK
В Square наши релизы создаются в рамках CI. Как мы видели ранее, запуск приложения для профилирования из Android Studio предполагает проверку параметра в конфигурации запуска. Как же сделать это с загруженным APK?
Оказывается, это возможно, но скрыто в File > Profile or Debug APK. Это откроет новое окно с разархивированным APK, в котором вы можете настроить конфигурацию запуска и начать профилирование.

Профилировщик Android Studio все замедляет
К сожалению, когда я тестировал эти методы в реальном приложении, профилирование из Android Studio сильно замедляло запуск приложения (примерно в 10 раз) даже на последних версиях Android. Я не знаю, почему так происходит. Возможно, в этом виновато “расширенное профилирование”, которое, похоже, не может быть отключено. Нам нужно найти другой способ!
Профилирование из кода
Вместо профилирования из Android Studio мы можем запустить трассировку непосредственно из кода:
val tracesDirPath = TODO("path for trace directory")
val fileNameFormat = SimpleDateFormat(
"yyyy-MM-dd_HH-mm-ss_SSS'.trace'",
Locale.US
)
val fileName = fileNameFormat.format(Date())
val traceFilePath = tracesDirPath + fileName
// Save up to 50Mb data.
val maxBufferSize = 50 * 1000 * 1000
// Sample every 1000 microsecond (1ms)
val samplingIntervalUs = 1000
Debug.startMethodTracingSampling(
traceFilePath,
maxBufferSize,
samplingIntervalUs
)
// ...
Debug.stopMethodTracing()Затем мы можем извлечь файл трассировки из устройства и загрузить его в Android Studio.
Когда начинать запись
Мы должны начать запись трейсов как можно раньше в жизненном цикле приложения. Как я уже говорил в Android Vitals — Разбираем холодный запуск, самый ранний код, который может запускаться при запуске приложения до Android P, - это ContentProvider, а на Android P+ - это AppComponentFactory.
До Android P / API < 28
class AppStartListener : ContentProvider() {
override fun onCreate(): Boolean {
Debug.startMethodTracingSampling(...)
return false
}
// ...
}<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name=".AppStartListener"
android:authorities="com.example.appstartlistener"
android:exported="false" />
</application>
</manifest>К сожалению, мы не можем контролировать порядок, в котором создаются инстансы ContentProvider, поэтому мы можем не захватить часть раннего кода запуска.
Примечание: @anjalsaneen указал в комментариях, что при определении провайдера мы можем установить тег initOrder, и провайдер с наибольшим этим значением будет инициализироваться первым.
Android P+ / API 28+
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name=".AppStartListener"
android:authorities="com.example.appstartlistener"
android:exported="false" />
</application>
</manifest><?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:appComponentFactory=".MyAppComponentFactory"
tools:replace="android:appComponentFactory"
tools:targetApi="p">
</application>
</manifest>Где хранить трейсы
val tracesDirPath = TODO("path for trace directory")API <28: широковещательный приемник (broadcast receiver) имеет доступ к контексту, в котором мы можем вызвать Context.getDataDir() для сохранения трейсов в каталоге приложения.
API 28: AppComponentFactory.instantiateApplication() отвечает за создание нового инстанса приложения, поэтому контекст пока недоступен. Мы можем захардкодить путь к /sdcard/ напрямую, хотя для этого требуется разрешение WRITE_EXTERNAL_STORAGE.
API 29+: при таргетинге на API 29 хардкод /sdcard/ перестает работать. Мы можем добавить флаг requestLegacyExternalStorage, но он не поддерживается в API 30. Примечание: Ясин Резгуй (Yacine Rezgui) предложил попробовать MANAGE_EXTERNAL_STORAGE в API 30+. В любом случае AppComponentFactory.instantiateClassLoader() передает ApplicationInfo, поэтому мы можем использовать ApplicationInfo.dataDir для сохранения трейсов в каталоге приложения.
Когда прекращать запись
В Android Vitals - первая отрисовка
