JankStats 程式庫

Stay organized with collections Save and categorize content based on your preferences.

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

功能

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

資源浪費經驗法則

雖然可以使用 FrameMetrics 來追蹤影格持續時間,但 FrameMetrics 無法協助判斷實際發生資源浪費的情況。JankStats 則具備可設定的內部機制,用於判斷發生資源浪費的時間,讓報表能夠提供更立即的幫助。

UI 狀態

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

如要解決這項問題,JnkStats 導入 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-alpha03"

接著,初始化並啟用每個 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 屬性。

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

您可以透過這些方法變更這兩個值,以便更貼近應用程式的情況,或是根據測試需求,強制指定測試期間是否發生資源浪費。

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
報告問題,幫助我們修正錯誤。