JankStats 程式庫

JankStats 程式庫可協助您追蹤及分析應用程式的效能問題。卡頓是指應用程式轉譯時間過長,JankStats 程式庫則針對應用程式的卡頓統計資料提供報告。

功能

JankStats 以現有的 Android 平台功能為基礎,包括 Android 7 (API 級別 24) 以上版本的 FrameMetrics API,或較舊 Android 版本的 OnPreDrawListener。這些機制可協助應用程式追蹤完成影格所需的時間。JankStats 程式庫提供兩項額外功能,讓資源更靈活且易於使用:卡頓經驗法則和 UI 狀態。

卡頓經驗法則

雖然可以使用 FrameMetrics 來追蹤影格持續時間,但 FrameMetrics 無法協助判斷實際發生卡頓的情況。JankStats 則具備可設定的內部機制,用於判斷發生卡頓時間,讓報表更能及時幫助使用者。

UI 狀態

通常需要瞭解應用程式中效能問題的背景。例如,如果您開發使用 FrameMetrics 的複雜多螢幕應用程式,發現應用程式經常有大量卡頓的影格,您需要將資訊放到背景來分析,瞭解問題發生的位置、使用者正在執行的操作以及如何複製此情況。

為解決這個問題,JankStats 導入了 state API,您可用來與程式庫通訊,提供應用程式活動的相關資訊。當 JankStats 記錄卡頓影格相關資訊時,會將應用程式的當前狀態納進卡頓報告。

使用方式

如要開始使用 JankStats,請為各個 Window 執行個體化並啟用資料庫。每個 JankStats 物件只會追蹤 Window 內的資料。如要將程式庫執行個體化,必須使用 Window 執行個體和 OnFrameListener 事件監聽器,兩者皆用於將指標傳送至用戶端。系統會在每個影格使用 FrameData 呼叫事件監聽器,並詳細說明:

  • 影格開始時間
  • 持續時間值
  • 是否該認定影格卡頓
  • 一組字串對,內含影格顯示期間的應用程式狀態相關資訊

如要讓 JankStats 更實用,應用程式應在資料庫中填入相關 UI 狀態資訊,用於 FrameData 中的報表功能。您可以透過 PerformanceMetricsState API (而非直接透過 JankStats) 完成這項操作,此 API 包含所有狀態管理邏輯和 API。

初始化

如要開始使用 JankStats 程式庫,請先將 JankStats 依附元件新增至 Gradle 檔案:

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

接著,初始化並啟用每個 Window 的 JankStats。此外,當活動在背景執行時,也應該暫停 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 物件後插入目前活動的狀態資訊。所有為此 JankStats 物件所建立的 FrameData 報表日後也會包含活動資訊。

JankStats.createAndTrack 方法會參照 Window 物件,該物件是 Window 中檢視區塊階層和 Window 本身的 Proxy。系統會透過特定執行緒呼叫 jankFrameListener,這個執行緒的用途是在內部將資訊從平台傳送至 JankStats。

如要為任何 JankStats 物件啟用追蹤和報告功能,請呼叫 isTrackingEnabled = true。雖然系統預設為啟用,但停用活動則會停用追蹤功能。在這種情況下,請務必先重新啟用追蹤功能再繼續。如要停止追蹤,請呼叫 isTrackingEnabled = false

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

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

報表

JankStats 程式庫會將每個影格的資料追蹤內容,全數傳遞至啟用中的 JankStats 物件 OnFrameListener。應用程式可以儲存並聚合這些資料,以供日後上傳。詳情請參閱「匯總」部分提供的範例。

您必須為應用程式建立及提供 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) 以上版本,則可使用下列程式碼公開顯示影格持續時間的更多資料:

使用事件監聽器中的 StateInfo,即可儲存應用程式狀態的相關資訊。

請注意,系統會透過特定執行緒呼叫 OnFrameListener,這個執行緒的用途是在內部將每個影格的資訊傳遞至 JankStats。在 Android 6 (API 級別 23) 以下版本中,系統會採用主要 (UI) 執行緒。在 Android 7 (API 級別 24) 以上版本中,系統則會採用為了供 FrameMetrics 使用而建立的執行緒。無論是哪一種情況,都請務必迅速處理回呼並傳回資料,以免執行緒發生效能問題。

另請注意,回呼中傳送的 FrameData 物件會重複用於每個影格,這樣就不必為資料報表分配新的物件。也就是說,您必須在其他位置複製及快取資料,因為在回呼傳回後,該物件應該立即視為無狀態且已過時。

匯總

您可能希望應用程式碼匯總每個影格資料,方便您自行儲存及上傳資訊。雖然儲存及上傳作業的詳細資料不在 Alpha 版 JankStats API 的範圍內,您仍可以查看初步活動,以便使用 GitHub 存放區中提供的 JankAggregatorActivity,將各個影格的資料匯總為更大的集合。

JankAggregatorActivity 使用 JankStatsAggregator 類別,將自身的報告機制分到 JankStats OnFrameListener 機制之上,為僅報告涵蓋許多影格的資訊集合提供更高層級的抽象層。

JankAggregatorActivity 不會直接建立 JankStats 物件,而是建立 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() 方法,您就不必在某個位置快取 holder 執行個體用於之後的擷取作業,而且還能更輕鬆地從程式碼中的任何位置擷取現有狀態物件,甚至是從無法以其他方式存取原始執行個體的程式庫程式碼擷取。

請注意,傳回值是 holder 物件,而不是狀態物件。holder 內部的狀態物件值只能透過 JankStats 設定。也就是說,如果應用程式針對包含該檢視區塊階層的 Window 建立 JankStats 物件,系統就會建立及設定狀態物件。相反地,如果不使用 JankStats 追蹤資訊,就不需要狀態物件,應用程式或程式庫程式碼也不必插入狀態。

這個方法可以擷取 holder,然後可由 JankStats 填入。外部程式碼隨時都可要求 holder。呼叫端可以快取這個輕量 Holder 物件,並隨時根據其內部 state 屬性的值設定狀態 (如下方程式碼範例所示),且狀態僅會在 holder 的內部狀態屬性並非空值時設定:

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

如要控制 UI/應用程式狀態,應用程式可以使用 putStateremoveState 方法插入 (或移除) 狀態。JankStats 會記錄這些呼叫的時間戳記。如果影格與狀態的開始和結束時間重疊,JankStats 就會一起回報這些狀態資訊和影格時間資料。

請針對所有狀態加入以下兩項資訊:key (狀態類別,例如「RecyclerView」) 和 value (當下發生的狀況,例如「scrolling」)。

狀態失效後,請使用 removeState() 方法移除狀態,確保不會連同影格資料一併回報錯誤或誤導性資訊。

如果使用先前新增的 key 呼叫 putState(),會將狀態的現有 value 替換為新的值。

狀態 API 的 putSingleFrameState() 版本會在下一個回報的影格中添加只記錄一次的狀態。系統隨後會自動移除這個狀態,確保您不會意外將過時的狀態加入程式碼中。請注意,由於 JankStats 會自動移除單一影格狀態,因此沒有與 singleFrame 同等的 removeState()

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 名稱。舉例來說,如果應用程式有五個不同的 RecyclerView,就可以為每個 RecyclerView 提供可識別的鍵 (而非一律只使用 RecyclerView),以便在產生的資料中輕鬆判斷影格資料所參照的執行個體。

卡頓經驗法則

如要調整內部演算法以判定卡頓情況,請使用 jankHeuristicMultiplier 屬性。

根據預設,系統會將卡頓定義為影格需花費兩倍時間,才能達到目前的刷新率效果。系統不會依據刷新率以外的因素判斷是否發生卡頓,因為應用程式轉譯時間的資訊不夠清楚。因此,建議您新增緩衝區,而且只在出現明顯的效能問題時才加以回報。

這兩種測試方法都可以透過變更這些方法,以更符合應用程式的情況,或是測試強制讓 jank 發生或不發生 (根據測試需要)。

Jetpack Compose 使用狀況

目前在 Compose 中使用 JankStats 幾乎不需要設定。請謹記以下設定,在設定變更時保持 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")
        }
    }
}

如需進一步瞭解如何在 Jetpack Compose 應用程式中使用 JankStats,請參考我們的效能範例應用程式

提供意見

歡迎透過下列資源與我們分享意見和想法:

Issue Tracker
報告問題,幫助我們修正錯誤。