Site icon AppTractor

Анализ сведений о запуске и завершении приложений в Android 15

В 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 из системных служб и с его помощью извлеките столько исторических данных, сколько их доступно в хранилище.

Доступные данные:

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.

Полный пример приложения можно найти в моем репозитории.

Спасибо за чтение!

Источник

Exit mobile version