В Android 11 появилась ApplicationExitInfo
, из которой можно узнать исторические причины, по которым приложение было выключено/завершено.
В Android 15 реализована новая ApplicationStartInfo
, которую можно использовать для анализа причин того, почему и как приложение было запущено.
С помощью этих данных можно проследить, как пользователи используют приложение. С помощью данных о выходе можно было проверить, не осталось ли в приложении проблем с выходом, а теперь и то, нет ли проблем с входом.
ApplicationStartInfo
Как уже было сказано, ApplicationStartInfo
была представлена в Android 15, поэтому она будет доступна только для устройств с API 35 и выше. Поэтому обязательно используйте соответствующую обработку версий или пометьте все методы с помощью @RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
.
Чтобы получить доступ к данным о стартах вам нужно сделать следующее:
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val startInfos = activityManager.getHistoricalProcessStartReasons(maxNumberOfInstances)
Получите ActivityManager
из системных служб и с его помощью извлеките столько исторических данных, сколько их доступно в хранилище.
Доступные данные:
- Причина запуска — уведомление, пользователь нажал на иконку в лаунчере, будильник…
- Состояние запуска — состояние, в котором приложение находится в данный момент — ошибка, запущено, первый кадр
- Тип запуска — холодная загрузка, теплая загрузка…
- Режим запуска — способ повторного использования или замены текущей активити — singleInstance, singleTask, singleTop, standard…
- Принудительная остановка — если запуск является первым с тех пор, как приложение было принудительно остановлено
- Интент — намерение, использованное для запуска активности
- Время запуска — карта временных меток событий во время запуска в наносекундах
val startType = startInfo.startType val startReason = startInfo.reason val startUpState = startInfo.startupState val launchMode = startInfo.launchMode val startTimestamp = startInfo.startupTimestamps val wasForceStopped = startInfo.wasForceStopped() val startIntent = startInfo.intent
Большинство значений — целые числа, которые определены как статические целочисленные константы в ApplicationStartInfo
.
Добавляем колбек
Вы можете добавить функцию обратного вызова, чтобы получать информацию, когда приложение полностью запущено, и получать информацию о запуске сразу, без запроса истории. Она возвращает список ApplicationStartInfo
.
val executor = context.mainExecutor activityManager.addApplicationStartInfoCompletionListener(executor) { // access the ApplicationStartInfo via `it` }
Добавляем события
Вы можете добавлять свои события в историю с помощью своего набора констант, которые должны быть больше 20 (START_TIMESTAMP_RESERVED_RANGE_SYSTEM) и меньше или равны 30 (START_TIMESTAMP_RESERVED_RANGE_DEVELOPER), согласно документации.
Время указывается в наносекундах.
val currentTimeInNanos = System.nanoTime() activityManager.addStartInfoTimestamp(25, timestamp)
System.nanoTime()
не ссылается на временные метки Unix, но если у вас есть 2 или более временных меток, это дает вам более детальное представление о производительности.
ApplicationExitInfo
Чтобы сделать эту статью полной, Android предоставляет способ получить информацию о том, как было завершено приложение. Он возвращает список ApplicationExitInfo
.
val exitInfos = activityManager.getHistoricalProcessExitReasons(packageName, 0, maxNum)
Работает почти так же, но вам нужно указать имя пакета из контекста, а также идентификатор процесса (PID), который можно оставить равным 0 для всех записей, и максимальное количество причин выхода, которые нужно получить.
Доступные данные:
- Причина выхода — причина, по которой приложение было остановлено
- Описание — человекочитаемая причина, по которой приложение было остановлено
- Важность выхода — насколько важным было приложение, когда оно было остановлено — передний план, дремота…
- Время остановки — временная метка в миллисекундах, когда приложение было остановлено
Причины выхода — это опять же в основном статические константные целые числа, определенные в ApplicationExitInfo
и ActivityManager.RunningAppProcessInfo
.
Пример использования
Большинство состояний и причин являются целыми числами, определенными в соответствующих классах как статические константы, поэтому мы обычно хотим использовать оператор when
для классификации результата.
Я предпочитаю использовать перечисления, поскольку целые числа не определяют ограничений, а операторы не могут быть исчерпывающими. Кроме того, их можно красиво сериализовать с помощью сериализации Kotlin.
Пример для начала:
enum class StartReason { START_REASON_ALARM, START_REASON_BACKUP, START_REASON_BOOT_COMPLETE, START_REASON_BROADCAST, START_REASON_CONTENT_PROVIDER, START_REASON_JOB, START_REASON_LAUNCHER, START_REASON_LAUNCHER_RECENTS, START_REASON_OTHER, START_REASON_PUSH, START_REASON_SERVICE, START_REASON_START_ACTIVITY; companion object { fun fromValue(value: Int): StartReason = when (value) { ApplicationStartInfo.START_REASON_ALARM -> START_REASON_ALARM ApplicationStartInfo.START_REASON_BACKUP -> START_REASON_BACKUP ApplicationStartInfo.START_REASON_BOOT_COMPLETE -> START_REASON_BOOT_COMPLETE ApplicationStartInfo.START_REASON_BROADCAST -> START_REASON_BROADCAST ApplicationStartInfo.START_REASON_CONTENT_PROVIDER -> START_REASON_CONTENT_PROVIDER ApplicationStartInfo.START_REASON_JOB -> START_REASON_JOB ApplicationStartInfo.START_REASON_LAUNCHER -> START_REASON_LAUNCHER ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS -> START_REASON_LAUNCHER_RECENTS ApplicationStartInfo.START_REASON_OTHER -> START_REASON_OTHER ApplicationStartInfo.START_REASON_PUSH -> START_REASON_PUSH ApplicationStartInfo.START_REASON_SERVICE -> START_REASON_SERVICE ApplicationStartInfo.START_REASON_START_ACTIVITY -> START_REASON_START_ACTIVITY else -> throw IllegalArgumentException("Unknown start reason value: $value") } } }
Аналогичным образом мы можем создать класс данных для отображения временных меток из стартовой информации:
data class StartupTimestamps( val applicationOnCreate: Long? = null, val bindApplication: Long? = null, val firstFrame: Long? = null, val fork: Long? = null, val fullyDrawn: Long? = null, val initialRenderThreadFrame: Long? = null, val launch: Long? = null, val reservedRangeDeveloper: Long? = null, val reservedRangeDeveloperStart: Long? = null, val reservedRangeSystem: Long? = null, val surfaceFlingerCompositionComplete: Long? = null ) { companion object { fun fromMap(timestampMap: Map<Int, Long>): StartupTimestamps = StartupTimestamps( applicationOnCreate = timestampMap[ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE], bindApplication = timestampMap[ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION], firstFrame = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME], fork = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FORK], fullyDrawn = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN], initialRenderThreadFrame = timestampMap[ApplicationStartInfo.START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME], launch = timestampMap[ApplicationStartInfo.START_TIMESTAMP_LAUNCH], reservedRangeDeveloper = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER], reservedRangeDeveloperStart = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START], reservedRangeSystem = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_SYSTEM], surfaceFlingerCompositionComplete = timestampMap[ApplicationStartInfo.START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE] ) } }
Или мы можем определить причины выхода:
enum class ExitReason { REASON_ANR, REASON_CRASH, REASON_CRASH_NATIVE, REASON_DEPENDENCY_DIED, REASON_EXCESSIVE_RESOURCE_USAGE, REASON_EXIT_SELF, REASON_FREEZER, REASON_INITIALIZATION_FAILURE, REASON_LOW_MEMORY, REASON_OTHER, REASON_PACKAGE_STATE_CHANGE, REASON_PACKAGE_UPDATED, REASON_PERMISSION_CHANGE, REASON_SIGNALED, REASON_UNKNOWN, REASON_USER_REQUESTED, REASON_USER_STOPPED; companion object { fun fromValue(value: Int): ExitReason = when (value) { ApplicationExitInfo.REASON_ANR -> REASON_ANR ApplicationExitInfo.REASON_CRASH -> REASON_CRASH ApplicationExitInfo.REASON_CRASH_NATIVE -> REASON_CRASH_NATIVE ApplicationExitInfo.REASON_DEPENDENCY_DIED -> REASON_DEPENDENCY_DIED ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> REASON_EXCESSIVE_RESOURCE_USAGE ApplicationExitInfo.REASON_EXIT_SELF -> REASON_EXIT_SELF ApplicationExitInfo.REASON_FREEZER -> REASON_FREEZER ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> REASON_INITIALIZATION_FAILURE ApplicationExitInfo.REASON_LOW_MEMORY -> REASON_LOW_MEMORY ApplicationExitInfo.REASON_OTHER -> REASON_OTHER ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE -> REASON_PACKAGE_STATE_CHANGE ApplicationExitInfo.REASON_PACKAGE_UPDATED -> REASON_PACKAGE_UPDATED ApplicationExitInfo.REASON_PERMISSION_CHANGE -> REASON_PERMISSION_CHANGE ApplicationExitInfo.REASON_SIGNALED -> REASON_SIGNALED ApplicationExitInfo.REASON_UNKNOWN -> REASON_UNKNOWN ApplicationExitInfo.REASON_USER_REQUESTED -> REASON_USER_REQUESTED ApplicationExitInfo.REASON_USER_STOPPED -> REASON_USER_STOPPED else -> throw IllegalArgumentException("Unknown exit reason value: $value") } } }
Не стесняйтесь заменить throw каким-либо значением по умолчанию, если хотите.
Аналогично с другими параметрами вы можете создавать дополнительные перечисления или другие классы данных по своему усмотрению. Для получения более подробной информации посетите документацию — ApplicationStartInfo и ApplicationExitInfo.
Полный пример приложения можно найти в моем репозитории.
Спасибо за чтение!