Библиотека JankStats

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

Возможности

JankStats основан на существующих возможностях платформы Android, включая API FrameMetrics в Android 7 (уровень API 24) и выше или OnPreDrawListener в более ранних версиях. Эти механизмы могут помочь приложениям отслеживать, сколько времени требуется для обработки кадров. Библиотека JanksStats предлагает две дополнительные возможности, которые делают ее более динамичной и простой в использовании: эвристика ошибок и состояние пользовательского интерфейса.

Янковая эвристика

Хотя вы можете использовать FrameMetrics для отслеживания длительности кадров, FrameMetrics не предлагает никакой помощи в определении фактических задержек. Однако JankStats имеет настраиваемые внутренние механизмы определения момента возникновения зависаний, что делает отчеты более полезными.

состояние пользовательского интерфейса

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

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

Использование

Чтобы начать использовать JankStats, создайте экземпляр и включите библиотеку для каждого Window . Каждый объект JankStats отслеживает данные только внутри Window . Для создания экземпляра библиотеки требуется экземпляр Window вместе с прослушивателем OnFrameListener , оба из которых используются для отправки метрик клиенту. Прослушиватель вызывается с FrameData для каждого кадра и детализирует:

  • Время начала кадра
  • Значения длительности
  • Следует ли считать кадр ненужным
  • Набор пар строк, содержащих информацию о состоянии приложения во время кадра.

Чтобы сделать JankStats более полезным, приложения должны заполнять библиотеку соответствующей информацией о состоянии пользовательского интерфейса для создания отчетов в FrameData. Вы можете сделать это через API PerformanceMetricsState (а не напрямую через JankStats), где находится вся логика управления состоянием и API.

Инициализация

Чтобы начать использовать библиотеку JankStats, сначала добавьте зависимость JankStats в файл Gradle:

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

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

class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metrics state holder can be retrieved regardless of JankStats initialization
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

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

Метод JankStats.createAndTrack принимает ссылку на объект Window , который является прокси для иерархии представлений внутри этого Window , а также для самого Window . jankFrameListener вызывается в том же потоке, который используется для внутренней доставки этой информации с платформы в JankStats.

Чтобы включить отслеживание и создание отчетов для любого объекта JankStats, вызовите isTrackingEnabled = true . Хотя по умолчанию эта функция включена, приостановка действия отключает отслеживание. В этом случае обязательно снова включите отслеживание, прежде чем продолжить. Чтобы остановить отслеживание, вызовите isTrackingEnabled = false .

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

Отчетность

Библиотека JankStats сообщает обо всех ваших отслеживаемых данных для каждого кадра в OnFrameListener для включенных объектов JankStats. Приложения могут хранить и объединять эти данные для последующей загрузки. Для получения дополнительной информации ознакомьтесь с примерами, представленными в разделе «Агрегация» .

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

private val jankFrameListener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}

Прослушиватель предоставляет покадровую информацию о заторах с помощью объекта FrameData . Он содержит следующую информацию о запрошенном кадре:

  • isjank : логический флаг, указывающий, произошло ли зависание в кадре.
  • frameDurationUiNanos : продолжительность кадра (в наносекундах).
  • frameStartNanos : время начала кадра (в наносекундах).
  • states : состояние вашего приложения во время кадра.

Если вы используете Android 12 (уровень API 31) или выше, вы можете использовать следующее, чтобы предоставить больше данных о длительности кадров:

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

Используйте StateInfo в прослушивателе для хранения информации о состоянии приложения.

Обратите внимание, что OnFrameListener вызывается в том же потоке, который используется внутри для доставки покадровой информации в JankStats. В Android версии 6 (уровень API 23) и ниже это основной поток (UI). В Android версии 7 (уровень API 24) и выше это поток, созданный и используемый FrameMetrics. В любом случае важно обработать обратный вызов и быстро вернуться, чтобы предотвратить проблемы с производительностью в этом потоке.

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

Агрегирование

Вероятно, вы захотите, чтобы код вашего приложения агрегировал данные по кадрам, что позволит вам сохранять и загружать информацию по своему усмотрению. Хотя подробности сохранения и загрузки выходят за рамки альфа-версии API JankStats, вы можете просмотреть предварительное действие по агрегированию покадровых данных в более крупную коллекцию с помощью JankAggregatorActivity , доступного в нашем репозитории GitHub .

JankAggregatorActivity использует класс JankStatsAggregator для наложения собственного механизма отчетов поверх механизма JankStats OnFrameListener , чтобы обеспечить абстракцию более высокого уровня для сообщения только о наборе информации, охватывающей множество кадров.

Вместо непосредственного создания объекта JankStats JankAggregatorActivity создает объект JankStatsAggregator , который внутри себя создает собственный объект JankStats:

class JankAggregatorActivity : AppCompatActivity() {

    private lateinit var jankStatsAggregator: JankStatsAggregator


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // Metrics state holder can be retrieved regardless of JankStats initialization.
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

Похожий механизм используется в JankAggregatorActivity для приостановки и возобновления отслеживания с добавлением события pause() в качестве сигнала для выдачи отчета с вызовом метода issueJankReport() , поскольку изменения жизненного цикла кажутся подходящим моментом для фиксации состояния джанк в приложении:

override fun onResume() {
    super.onResume()
    jankStatsAggregator.jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    // Before disabling tracking, issue the report with (optionally) specified reason.
    jankStatsAggregator.issueJankReport("Activity paused")
    jankStatsAggregator.jankStats.isTrackingEnabled = false
}

Приведенный выше пример кода — это все, что нужно приложению для включения JankStats и получения данных кадра.

Управляйте государством

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

Этот статический метод извлекает текущий объект MetricsStateHolder для данной иерархии представления.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Можно использовать любое представление в активной иерархии. Внутренне это проверяет, существует ли существующий объект Holder , связанный с этой иерархией представлений. Эта информация кэшируется в представлении наверху этой иерархии. Если такого объекта не существует, getHolderForHierarchy() создает его.

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

Обратите внимание, что возвращаемое значение является объектом-держателем, а не самим объектом состояния. Значение объекта состояния внутри держателя устанавливается только JankStats. То есть, если приложение создает объект JankStats для окна, содержащего эту иерархию представлений, тогда создается и устанавливается объект состояния. В противном случае, если JankStats не отслеживает информацию, нет необходимости в объекте состояния, и коду приложения или библиотеки не обязательно вводить состояние.

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

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

Чтобы управлять состоянием пользовательского интерфейса/приложения, приложение может внедрить (или удалить) состояние с помощью методов putState и removeState . JankStats регистрирует временные метки этих вызовов. Если кадр перекрывает время начала и окончания состояния, JankStats сообщает эту информацию о состоянии вместе с данными о времени для кадра.

Для любого состояния добавьте две части информации: key (категория состояния, например «RecyclerView») и value (информация о том, что происходило в данный момент, например «прокрутка»).

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

Вызов putState() с добавленным ранее key заменяет существующее value этого состояния новым.

Версия API состояния putSingleFrameState() добавляет состояние, которое регистрируется только один раз, в следующем отчетном кадре. После этого система автоматически удалит его, гарантируя, что в вашем коде случайно не появится устаревшее состояние. Обратите внимание, что не существует эквивалента removeState() для SingleFrame, поскольку JankStats автоматически удаляет состояния одного кадра.

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        // check if JankStats is initialized and skip adding state if not
        val metricsState = metricsStateHolder?.state ?: return

        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                metricsState.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

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

Янковая эвристика

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

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

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

Использование в Jetpack Compose

В настоящее время для использования JankStats в Compose требуется очень мало настроек. Чтобы сохранить PerformanceMetricsState при изменении конфигурации, запомните его следующим образом:

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

А чтобы использовать JankStats, добавьте текущее состояние в stateHolder , как показано здесь:

val metricsStateHolder = rememberMetricsStateHolder()

// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
    snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
        if (isScrolling) {
            metricsStateHolder.state?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

Для получения полной информации об использовании JankStats в вашем приложении Jetpack Compose ознакомьтесь с нашим примером приложения производительности .

Оставьте отзыв

Поделитесь с нами своими отзывами и идеями через эти ресурсы:

Трекер проблем
Сообщайте о проблемах, чтобы мы могли исправить ошибки.
{% дословно %} {% дословно %} {% дословно %} {% дословно %}