工具與成效

在 Compose 中超越自我

喇叭

關於

講座簡介

Jetpack Compose 可讓您輕鬆設計高品質的使用者介面,但可能難以追蹤影響 UI 效能的問題。這類問題可能會導致使用者注意到問題,例如啟動延遲,以及顯示為捲動卡頓或動畫不流暢的影格遺失。

在這場講座中,我們將介紹一些工具,協助您找出影響 UI 效能的因素,重點在於 Perfetto 和 (重新) 組合追蹤。接著,我們會探討您可能正在使用哪些反模式,導致成本在不知不覺中增加,以及如何在程式碼中找出這些反模式,並改用哪些正模式。聽完這場演講後,您將瞭解如何以效能為考量,在 Compose 中思考,以及如何在應用程式中剖析、分析及驗證效能最佳化。

摘要

  • Perfetto 追蹤記錄會記錄裝置端活動,並沿著主執行緒顯示 Compose 切片,例如組合和版面配置。您可以使用這些工具檢查影格時間軸,並追蹤階段持續時間。
  • 可組合函式中的每項作業都有成本,包括啟動和取消協同程式。稽核副作用 API,並考慮合併具有相同索引鍵的效果,減少組合負擔。
  • Composition 追蹤會為可組合函式加上註解,而重組追蹤 (1.13 以上版本) 則會追蹤導致重組的狀態讀取和寫入。這類事件可協助您直接在 Perfetto 追蹤記錄中診斷非預期的工作。
  • ComputedState (Compose 1.13 以上版本) 可避免不必要的撤銷,且在讀取後寫入撤銷期間,通常比 DerivedState 更快。如果昂貴的計算或分配作業可透過快取受益,DerivedState 仍會發揮作用。
  • 略過可組合項會產生維護成本和參數比較負擔。請依賴強大的略過功能和物件身分相等性,或完全避免失效,而不是強制穩定。

技能

資源

Gemini Notebook

主題

  • jetpack-compose
  • android-performance
  • perfetto
  • 基準化
  • 協同程式
  • composition-tracing
  • android-cli

轉錄稿

簡介

  • 您好!

  • 大家好。

  • 希望你有充分休息。

  • 希望你已經喝過咖啡、做好保濕,一切準備就緒。

  • 今天我們會深入探討。

  • 沒錯,我們開始吧。

  • 今天會深入探討。

  • 我是 Andrei Shikov,今天與 Andrew Bailey 一起上台。

  • 我們都在 Compose 編譯器執行階段團隊工作,但也負責許多其他工作,包括效能。

  • 今天的主題是回答各位對效能的問題,因為 Jetpack Compose 已成為 Android 開發人員的預設 UI 工具包。

  • 現在,許多人都在詢問如何確保 Compose UI 盡可能發揮效能。

  • 如前所述,今天我們會深入探討。

  • 此外,我們也會介紹一些工具,協助您在 Compose 中發揮更出色的表現。

  • 首先,我想談談為什麼效能對我們很重要。

  • 在思考品質和打造優質應用程式時,您通常會想到動畫,我們則會想到豐富的功能集,有時會忽略效能,但使用者觀感確實取決於應用程式的效能。

  • 因此,如果動畫卡頓,使用者對應用程式的觀感會立即下降。

  • 因此,我們今天提到的所有指標和數字,實際上都是我們選取的代理指標,用來代表使用者對應用程式、所有動畫以及畫面上發生的所有事情的感受。

  • 因此,最佳化應用程式基本上就是最佳化使用者體驗。

  • 今天我們會深入探討許多主題,希望大家能抱持這種心態。

  • 現在,當你開始查看應用程式的效能時,我希望你快速完成三件事,三件快速的事。

  • 首先,如要評估效能,請在發布模式下進行,這樣就不會有大量可偵錯資訊,也不會拖慢應用程式速度。

  • 第二,啟用 R8 或其他縮小工具。

  • 今年,我們推出一項最佳化功能,可將協同程式效能提升兩倍,此外也做了許多其他努力。

  • 最佳化 Compose 時,我們非常仰賴 R8。

  • 最後,請務必使用基準設定檔,尤其是啟動指標。

  • 因此,在應用程式啟動時,系統會預先編譯並預先載入您在啟動時載入的類別。

  • 如您所見,這與應用程式的一般可偵錯版本相差 6 倍。

  • 當您評估所有這些項目時,它們會與您在程式碼中執行的其他勝出項目結合。

  • 現在,您已完成所有操作,但應用程式仍有點延遲,或仍有其他效能問題,這時您就需要進入這個效能迴圈。

  • 您想開始查看特定情境的一些指標。

  • 舉例來說,假設應用程式載入時間過長,您可能需要重設應用程式,從一般啟動器啟動,並檢查應用程式需要多久時間才能顯示第一個資料畫面。

  • 或者,應用程式可能有點卡頓,因此您捲動畫面,並確保捲動操作可重複且穩定。

  • 因此,本投影片中的穩定環境非常重要,因為在後續進行最佳化時,您需要確保指標有所改善,並確認這些最佳化措施確實能達到預期效果。

  • 基準測試非常適合用於這項作業,因為基準測試已經擷取大量資料,並執行多次疊代,確保裝置中沒有太多雜訊,而且還會執行許多裝置設定,對您很有幫助。

  • 此外,基準測試預設也是可重複執行的情境,這正是我們在這裡努力達成的目標。

  • 但您也可以視需要使用多個指令碼。

  • 最後,您要剖析應用程式、改善應用程式中的某些內容、查看資料,然後再次重複,並可能針對應用程式遇到困難的不同情境指標再次重複。

使用 Perfetto 追蹤記錄進行分析

  • 查看資料時,今天介紹的第一項工具是 Perfetto 追蹤記錄,請務必熟悉這項工具。

  • Perfetto 追蹤記錄會記錄追蹤期間裝置上發生的所有事件,例如執行 Macrobenchmark 或錄製指令碼時。

  • 對所有致力提升 Android 效能的工程師來說,這項工具非常有幫助。

  • 我現在大部分的工作時間都在查看 Perfetto 追蹤記錄。

  • 看起來像這樣。

  • 資料量相當龐大。

  • 如前所述,這類記錄會擷取裝置上發生的所有事件。

  • 因此,一開始您可能很難找到應用程式,建議您使用這個方便的搜尋列。

  • 搜尋 Compose、搜尋應用程式套件等。

  • 只要專注於熟悉的事物,就能讓自己平靜下來。

  • 當您找到應用程式程序後,就會看到影格時間軸,以及應用程式中許多作業所需的時間。

  • 如果我們隔離其中一個,您會看到類似這樣的內容。

  • 第一項是主執行緒工作。

  • Compose 的大部分作業都在這裡進行。

  • 接著是 RenderThread。

  • 我們在這裡處理畫布指令,並將其編碼至 GPU。

  • 接著,GPU 會執行工作,然後系統會進行合成。

  • 今天我們只會探討主執行緒。

  • 因為這是 Compose 運作的地方。

  • 我們討論的是 Compose,因此只著重於這點是合理的。

  • 當我們在主執行緒上隔離該影格時,實際上會開始看到 Compose 切片。

  • 我為你挑選了幾張。

  • 就像組合、測量、版面配置、繪圖。

  • 如果您聽過 Compose 階段,並曾查看成效,可能會看到這些階段。

  • 因此,Perfetto 追蹤記錄中會有個別切片,您可以輕鬆找出每個階段所需的時間,以及在每個特定影格中執行的階段。

  • 我們已經詳細討論過如何最佳化部分工作,您需要確保在正確的階段執行正確的工作。

  • 舉例來說,如果您有動畫會更新方塊顏色或類似內容,您只需要撤銷繪製階段,而不是組合結構和版面配置等。

  • 這是效能 101,今天我們會深入探討一些內容,特別是組合結構。

  • 現在請 Andrew 為我們說明。

  • 謝謝 Andrei。

  • 正如 Andrei 所說,我們之前花了很多時間討論效能最佳化,例如避免向後讀取、確保將狀態讀取延遲到組合生命週期的正確階段,以及最佳化和確保可組合項按照預期失效,這樣就不必在每個影格上執行太多工作。

  • 這是很好的起點,但我們已經有很多相關資料,因此想進一步探討這個主題。

  • 因此,今天我們將探討在完成這些最佳化作業後,組合中還剩下什麼,以及可組合項本身在做什麼。

  • Compose 程式碼中的所有內容,以及應用程式隨附的所有內容,在效能方面都有一個不幸的事實,那就是所有內容都有相關成本。

  • 應用程式中提供的程式碼都不是免費的,即使是讀取狀態等微不足道的事,以及在組合結構中組合副作用等,都會產生一些實際成本,因此請務必留意這些成本,尤其是在開始剖析可組合函式實際執行的作業,並開始在應用程式本身中擠出更多效能時。

  • 為了在最微觀的尺度上呈現實際情況,以下列出一些您可以在程式碼中任何位置執行的最小作業,特別是在可組合函式中,而在這個解析度下,我們談論的是奈秒尺度,也就是這些作業大致上需要多少成本。

  • 如要提升成效,有兩種做法。

  • 您可以移除大量低成本作業,或少量高成本作業。

  • 具體來說,在這張圖表中,協同程式是這些作業中成本較高的部分,因此很適合做為開始尋找的範例,因為如果我們移除其中幾個執行個體,就能更快在應用程式中看到更具體的效益。

  • 關於協同程式,還有一點要特別注意,如投影片所示,啟動協同程式會產生費用,取消協同程式也會產生費用。

  • 取消成本特別值得注意,因為這主要發生在您處置內容,以及可能移至應用程式內其他區域時。

  • 在許多應用程式中,離開頁面是非常耗費資源的情況,您需要盡可能縮短影格時間,並確保充分利用現有資源。

  • 因此,請務必留意這兩項費用。

  • 無論我們看的是什麼,所有這些都會在這個大約 1600 萬奈秒的規模上累加。

  • 因此,我們需要減少一些預算,而協同程式是很好的起點。

管理副作用

  • 在 Compose 中,協同程式最常與 Effects API 搭配使用。

  • 這裡有一系列 API,可與 SideEffects 搭配使用,從高功能到低延遲都有。

  • 在這個光譜中,功能最齊全的是 LaunchedEffect,其中包含非同步工作的所有內容,尤其是動畫,以及任何需要協同程式範圍的內容。

  • 如果不需要這項功能,且能以較便宜的 API 完成工作,則可考慮使用,前提是符合您的用途需求。

  • 為了實際展示,我準備了一個媒體播放器可組合項,用來示範所有不同的 SideEffect,以及這些效果可能出現的時機。

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • 我們可以先從 LaunchedEffect 開始。

  • 在這個範例中,這個方法用於在媒體播放器中非同步載入資料。

  • 如果您需要協同程式範圍,或一般需要協同程式或發出暫停呼叫,請使用 LaunchedEffect。

  • 雖然這項操作仍屬於較昂貴的操作,且協同程式會產生費用,但只要您能善用並證明其合理性,請使用合適的 API 執行工作。

  • 不過,如果您發現自己使用 LaunchedEffect,但未運用協同程式,建議您考慮使用其他 API,這些 API 可以執行相同操作,但成本較低。

  • 因此,我們可以在光譜中看到其他副作用,例如 DisposableEffect。

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • 如果您的效果有相關聯的設定和拆解程序,DisposableEffect 會非常實用。

  • 同樣以媒體播放器為例,我有一個範例,會在可組合項存在時,將活動方向鎖定為橫向,並在可組合項消失後解除方向鎖定。

  • 如果您想使用 LaunchedEffect 和 DisposableEffect 的一次性效果,但不需要協同程式,也沒有 dispose 效果,也可以使用我們在 Compose 1.12 中推出的新 SideEffect with keys API。

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • 如果您已使用其他兩個 API,這個 API 是很好的替代方案。無論是為了方便而使用 LaunchedEffect,還是為了單次效果而使用 DisposableEffect,使用金鑰 API 導覽或遷移至 SideEffect 都能達到相同效果,而且負擔更低、效能更佳,尤其是在整個應用程式中多次遷移時。

  • 最後,最便宜的 API 是標準 SideEffect API。

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • 如果想將外部狀態與組合中的狀態同步,這個方法最合適。

  • 同樣以媒體播放器為例,我還有另一個範例,說明如何將部分播放狀態與組合結構接收到的部分參數同步。

  • 請注意,如果您決定稽核附帶效果的使用情形,並改用成本較低的效果,除了我們討論過的其他行為和用途外,附帶效果的排序保證也有所不同。

  • 所有可處置的效果都會先執行,接著是一般副作用 (無論是否帶有索引鍵),然後在影格完成後,所有啟動的效果都會執行,因為涉及調度。

  • 如果您需要依據這個時間點啟動效果,請在考慮遷移時留意這一點。

  • 無論進行何種遷移,請務必測試程式碼,確保不依賴任何時間保證,且程式碼在遷移後仍可正常運作。

  • 如果無法降低附帶影響本身的複雜度,您也可以將附帶影響合併,減少附帶影響的數量。

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • 如果有多個效果使用同一個金鑰,就特別需要考量這一點。

  • 由於這兩項工作可以同時執行,因此很適合合併。

  • 在這個範例中,我有多個啟動效果,每個效果都會對某個檢視模型發出非同步呼叫。

  • 如果我將這些函式替換為單一 LaunchedEffect,然後在其中加入其他啟動呼叫,即使這裡執行的協同程式數量相同,負擔仍會減少,因為這會將工作從組合結構中移出,並移至協同程式中。

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • 因此,在這種情況下,建議您考慮在適當位置移入,盡可能減少效果數量。

  • 效果和協同程式只是您在應用程式中可能稽核的一個例子,但每個可組合項和每個應用程式都會有所不同,且其中會有不同的機會。

  • 為了瞭解哪些內容與您的應用程式相關,我們需要討論 Andrei 接下來要介紹的工具,也就是組合追蹤。

組合結構和重新組合追蹤記錄

  • 現在我們來做個小測驗

  • 因此,組合追蹤功能 (如果您查看 Perfetto 框架的忠實重現,我們之前在不同的投影片中看過) 會提供不同階段的時間資訊,但不會提供太多關於發生情況的詳細資料。

  • 什麼?

  • 沒錯,就是組合中執行的可組合函式。

  • 而組合追蹤功能可以協助我們。

  • 因此,我們在 Compose 編譯器中引入一些標記,基本上會為每個函式加上註解,並在 Perfetto 追蹤記錄中顯示,以便您查看每個函式所需的時間。

  • 您可以使用這項功能,找出確切重組的函式。

  • 不過,這會產生一些負擔,因此如果您想擷取最準確的成效資料,建議使用一般 Perfetto,或在不擷取 Perfetto 的情況下執行,並使用其他指標。

  • 不過,如果您想瞭解每個可組合項所需的時間,組合追蹤功能就非常實用。

  • 有時您也想瞭解為何會重新組成,這正是我們在下一個 Compose Tracing 執行階段追蹤版本 (即 1.13) 中嘗試解決的問題。

  • 我們現在推出重組追蹤功能,可為導致重組的每個狀態讀取和狀態寫入加上註解。

  • 因此,您會在 Perfetto 追蹤記錄中看到類似這樣的內容,其中會顯示每個狀態寫入,以及寫入和讀取發生的呼叫堆疊,以便您將這些狀態與程式碼中存取或修改這些狀態的實際行對應。

  • 如要進一步瞭解,請點選這裡的連結。

  • 其中包含組合追蹤和重組追蹤的所有說明文件。

  • 現在,Andrew 將說明如何更有效率地使用這項功能,避免狀態寫入。

狀態最佳化和失效

  • 謝謝 Andrei。

  • 根據這項調查的結果,您可能會發現組合結構中有無效項目,這些項目對應用程式沒有用處,而且一開始就可以避免。

  • 舉例來說,最常見的例子是「捲動至頂端」按鈕,其顯示與否取決於可見度或捲動進度。

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • 在這裡,由於我正在讀取組合中的捲動狀態值,因此這是一個非常頻繁變更的狀態,可能會在每個影格中變更一次,這會導致許多可避免的重組,因為這個條件,即使捲動狀態本身正在變更,我們在這裡關注的實際條件不會經常變更。

  • 在這種情況下,建議您使用運算狀態等功能,避免應用程式因不重要的狀態變更而失效。

  • 在這個範例中,我將計算本身包裝在計算狀態中,當我們讀取這個狀態時,系統不會根據捲動位置本身撤銷,而是會根據整個條件有條件地撤銷這個可組合項,並從其他狀態本身衍生。

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • 如果您曾看過這個模式,可能也看過它與衍生狀態搭配使用。

  • ComputedState 是 Compose 1.13 的新功能,雖然 ComputedState 和 DerivedState 的目標相似,都是在計算結果變更時有條件地失效,但兩者的做法有細微差異。

  • DerivedState 是 Compose 推出以來就有的功能,可快取計算出的值並重複使用,直到依附元件變更為止。

  • 使用 ComputedState 時,每次讀取值都會重新計算。

  • 快取聽起來很有用,但實際上在許多情況下,快取的費用比直接讀取結果還要高。

  • 為了瞭解這種情況,我們可以舉例說明如何在應用程式中使用這些狀態,並深入探討每個狀態的實際作用。

  • 這裡我有一個狀態,大致反映了我們在「捲動至頂端」按鈕中的操作,其中有一些條件是基於其他狀態,我同時將其設為 DerivedState 和 ComputedState。

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • 接著,我會讀取狀態兩次,兩次都會取得相同的值,但兩者在幕後執行的作業不同。

  • 為了簡化 Perfetto 等工具中實際顯示的追蹤記錄,我們可以檢視讀取這兩種狀態時實際發生的情況。

  • 首先,我們呼叫 computedState.value。

  • 系統會執行計算 lambda,然後讀取狀態值,並進行一些維護作業,通知系統已讀取。

  • Compose 中的所有快照狀態都是如此。

  • 第二次讀取的行為與第一次讀取完全相同,且全程具有相同的效能特性。

  • 相較之下,DerivedState 的第一次讀取會執行類似的操作,仍須呼叫計算 lambda 並執行其他通知,但會產生一些快取管理負擔,第一次執行時會稍微短路,因為沒有快取可檢查。

  • 因此,它會對快取本身進行一些維護,然後在稍長的時間內完成相同的工作。

  • 不過,DerivedState 的第二次讀取確實比 ComputedState 的第二次讀取快,即使這兩個狀態都相當簡單。

  • 如前幾張投影片所述,在某些情況下,快照讀取的成本不容忽視,而從快取讀取實際上比執行讀取本身更快,這就是為什麼我們看到 DerivedState 的第二次讀取本身略有優勢。

  • 但實際有趣的情況是,當狀態需要失效時會發生什麼事。

  • 這就是所謂的「寫入後讀取」情境,也就是更新計算中使用的其中一個狀態,且必須再次執行計算。

  • 在程式碼中,這看起來就像我們修改 a,可能遞增它,然後再次讀取值或重新組合。

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • 在實際圖表中,ComputedState 的外觀也完全相同。

  • 它沒有要管理的快取,無論是第幾次讀取,看起來都完全一樣。

  • 不過,DerivedState 的任務可不輕鬆。

  • 它會經過一條非常昂貴的路徑,在更新和呼叫計算本身之前,先確認快取無效,然後必須進行依附元件追蹤,並管理大量依附元件和觀察,才能寫回快取值,並瞭解所有這些值實際上都已變更,並管理維護本身。

  • 所有這些因素加起來,與 ComputedState 相比,撤銷 DerivedState 的成本非常高。

  • 為了用數字佐證上述所有說法,ComputedState 首次讀取的速度通常會快約 200 奈秒。

  • 在 Pixel 6 上,ComputedState 在特定基準測試條件下,會慢約 350 奈秒。

  • 因此,在第二次讀取時,ComputedState 的效能會略低於 DerivedState。

  • 不過,在我們實際關注的情況下,也就是我們撤銷的情況,以及您可能需要在影格上執行工作的狀況,ComputedState 的速度幾乎是 DerivedState 的兩倍以上。

  • 因此,視狀態的用途而定,ComputedState 是非常適合的首選。

  • 一般來說,使用 DerivedState 和 ComputedState 時,仍須做出決定。

  • 我們不會淘汰 ComputedState,兩者仍有用途。

  • 一般來說,如果不確定要選擇哪一個,預設選擇運算狀態會是個好選擇。

  • 衍生狀態最實用的地方在於,您實際上可以利用快取,也就是說,您可以執行一些非一般作業,例如複雜的演算法、一些大型分配、許多狀態讀取,這些作業實際上都受益於這裡的快取,或者任何非一般作業或取決於物件相等性和物件身分相等性等事物,您可以在其中彌補差異,並獲得衍生狀態提供的快取優勢。

Compose 的略過和穩定性

  • 另一個我想談論的明顯問題是,衍生狀態和運算狀態都試圖避免使可組合項本身失效。

  • 這與略過可組合函式不同,後者通常是指有條件地撤銷,或不執行可組合函式內的工作。

  • 我想在這裡揭露的真相是,雖然 Compose 內部有很多關於穩定性和略過的討論,但這並非最終目標,也不一定是最佳化的正確做法。

  • 如前所述,組合中的所有項目都會產生一些相關成本,包括略過。

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • 略過不一定代表不重要。

  • 我們必須進行大量維護工作,才能啟用這項功能。

  • 我們必須執行一些操作,例如儲存所有參數,並在初始組合結構中為這些參數尋找空間,然後在重新組合時更新該記錄。

  • 我們也必須比較您的值與先前的值,瞭解可組合項是否應略過。

  • 接著,所有這些都只是 if 檢查的功能,可略過程式碼或不略過。

  • 視實際情況而定,這項作業可能相當昂貴。

  • 編譯器在幕後進行了最佳化,如果您傳遞相同的參數,我們可以重複使用相等檢查,但這並非萬能,而且很容易被擊敗。

  • 如要查看略過機制不盡如人意的範例,可以參考一個非常經典的範例,也就是捲動清單,其中包含一些大型物件,內含要在畫面上組成的所有資料。

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • 因此,這裡的畫面模型包含標題畫面等內容,以及所有聯絡人清單,然後透過幾個可組合項傳遞,最終呼叫延遲清單,然後透過一些項目儲存格將其顯示在畫面上。

  • 這段程式碼的組合結構階層大致如下:同樣從聯絡人畫面開始,稍微展開,然後有一些其他可組合項會呼叫並顯示項目。

  • 如果觀察資料傳遞方式,會發現資料是從頂層的模型開始傳遞。

  • 我們將其解構為字串,然後是清單,然後將該清單轉換為延遲清單的項目 lambda,然後延遲清單將組成畫面上顯示的所有項目。

  • 我們來看看修改這份清單中最後一個聯絡人時,會發生什麼情況。

  • 我們希望只撤銷清單中的最後一個項目,但實際情況並非如此。

  • 如果查看這項重組作業的組合追蹤記錄,會看到類似這樣的內容:從聯絡人畫面開始,最後向下捲動至這個清單項目。

  • 而且這種方式相當昂貴。

  • 我們也可以看到,追蹤記錄中似乎有許多區段沒有任何活動。

  • 雖然這裡並不明顯,但這實際上是可組合項決定是否要略過的地方。

  • 在這個範例中,由於 Lazy 清單重組的方式,只有兩個可組合項會略過。

  • 當您提供新的內容 lambda 時,系統會撤銷其擁有的所有項目。

  • 因此,除了變更的聯絡人外,未變更的兩個聯絡人也會失效,而這兩個聯絡人是這裡唯一可以略過的項目。

  • 其他所有項目都會略過等式比較,只為了瞭解實際執行的內容。

  • 如果物件穩定,這項比較作業實際上可能相當耗費資源。

  • 如果強制我們檢查所有資料結構,實際成本可能會增加,而且只是為了判斷兩個物件是否相等,就可能需要付出不小的代價。

  • 更糟的是,由於我們已解構,實際上必須對清單進行兩次相同的比較。

  • 如果讓編譯器採用預設行為並使用強略過模式,您仍可略過可組合項,並檢查是否要依條件略過可組合項,但系統會採用更快速的方法,使用物件身分相等性,而非結構相等性。

  • 這樣一來,我們就能更快計算,即使我們在這個影格上重新組合相同的項目,但實際上我們能快速進入,不會浪費太多時間來確定我們無法略過這些可組合項。

  • 這也是為什麼我們強調使用基準和追蹤記錄等工具,瞭解應用程式內部情況,而不是直接重組計數。

  • 雖然這些指標是不錯的參考依據,但最佳化應用程式時,不一定要過度關注或過度重視。

  • 請記住,與其嘗試略過可組合函式,不如一開始就避免使其失效。

使用方法追蹤記錄分析執行作業

  • 在這個範例中,我們發現組合追蹤記錄可以提供許多有關發生情況的資訊,但也不一定能顯示完整情況。

  • Andrei 將為我們介紹方法追蹤記錄。

  • 現在我們來做個小測驗

  • 正如 Andrew 所說,Composition Tracing 不一定能提供所有資訊,因為它只會標註可組合函式,而且在某些情況下,Composition Trace 中只會顯示空白空間,雖然這已經擴充了 Perfetto Trace,但仍無法顯示所有資訊。

  • 有時可能是 Compose 造成的,有時可能是程式碼中的問題,光看這裡的組合追蹤記錄,真的很難找出原因。

  • 現在,您要擷取方法追蹤記錄。

  • Android Studio 的分析器中有個項目,或者你也可以使用 ADB 指令擷取追蹤記錄,這樣就能取得 Java 環境中發生的每個函式呼叫。

  • 包括 Compose 編譯器在其中編碼的所有內容、Compose 執行階段執行的所有內容,以及程式碼執行的所有內容,這對於追蹤、調查實際執行的內容,以及在該空間中呼叫的函式非常有用。

  • 我想請大家看看中間這個項目。

  • 我想提醒大家,方法追蹤記錄的負擔非常高。

  • 根據函式樹狀結構的複雜程度,負擔會增加 10 到 20 倍,因為每次函式呼叫都會增加一點負擔。

  • 請看螢幕截圖。

  • 這通常是應用程式啟動時的方法追蹤記錄,您可能會問:「如何解讀這項資訊?」

  • 一般來說,您會先查看這裡的函式,開始追蹤組合結構。

  • 這就像簡化的組合追蹤,其中包含 ScreenContent、一些按鈕和其他函式。

  • 假設我們想瞭解 ScreenContent 中發生的情況。

  • Perfetto 有搜尋列。

  • 您在 Perfetto 檢視器中載入方法追蹤記錄,搜尋與此處相同的函式 ScreenContent,就會看到類似的內容,也就是畫面的方法追蹤記錄。

  • 如你所見,這裡有 Composer.startGroup 和群組通話。

  • 執行階段和編譯器會在背景執行這項作業,確保組合結構能實際發揮作用,但這裡有趣的呼叫是 Composer.changed。

  • 這樣一來,我們就能檢查可組合函式在先前和目前叫用之間,參數是否有所變更。

  • 而這些Composer.changed中,大部分都是由這個ScreenModel.equals主導。

  • 因此,ScreenModel 可能是某種大型資料類別,我們可能將其設為穩定,以協助重組計數,因此它可能會出現在編譯器報告中。

  • 在這個特定情況下,移除這個穩定註解可能有助於提升效能。

  • 因此,您需要找出這類問題,而組合方法追蹤記錄是找出這類問題的最佳工具。

  • 現在我們來做個小測驗

  • 今天我們討論了許多內容,現在請 Andrew 為我們總結。

快速的 Compose 設計和效能工具

  • 最後,我們來總結一下今天談到的所有內容。在設計可組合項和最佳化現有應用程式時,請務必留意以下幾點。

  • 首先,請記住,成本較低的可組合項和未失效的可組合項,比略過的可組合項更好。

  • 請注意,設計可組合項時,最好考慮使用較小且更容易比較的引數,並盡可能重複使用與其他函式呼叫的比較,以充分利用最佳化功能。

  • 在可組合函式中使用 API 時,請注意,效能或影格時間會受到 API 本身的成本和使用頻率影響。

  • 請留意這些成本的累積方式,並注意可能會有相關的拆除成本,而這些成本可能會在最不適合應用程式的時間點產生。

  • 此外,請務必遵守規範,在應用程式內進行最佳化。

  • 首先,請進行基準測試,以便做出明智決策,並剖析應用程式,確保您指定正確的程式碼區域,並驗證您正在進行變更,並查看您預期的修改結果,這些修改是基準測試和剖析的結果。

  • 此外,請注意,應用程式的可維護性和效能之間可能需要取捨。

  • 因此,請採取最適合您和團隊的做法,確保不會過度最佳化,導致維護工作陷入困境。

  • 此外,您還可以使用現有的 AI 代理,取得更多協助。

  • 我們已推出 Android CLI,並在其中提供 Jetpack Compose 效能技能,其中包含我們今天談到的許多資訊,可協助您檢查及稽核應用程式,並針對這類最佳化提供建議。

  • 您可以使用 Android CLI,在現有代理中安裝。

    android skills add jetpack-compose-performance
  • 如要進一步瞭解 Android CLI,請前往 developer.android.com。

  • 您也可以前往 GitHub 存放區,進一步瞭解我們的技能。

  • 最後,祝您安全地進行基準測試,並愉快地編寫程式碼。

程式碼

程式碼片段

「管理副作用」中 09:34 的 Kotlin 程式碼片段
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
「管理副作用」中 10:15 的 Kotlin 程式碼片段
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
「管理副作用」中 10:36 的 Kotlin 程式碼片段
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
「管理副作用」影片 11:12 處的 Kotlin 程式碼片段
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
「管理副作用」中 12:12 的 Kotlin 程式碼片段
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
「管理副作用」中 12:36 的 Kotlin 程式碼片段
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
「狀態最佳化和失效」影片 15:12 處的 Kotlin 程式碼片段
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
「狀態最佳化和失效」影片中 15:51 的 Kotlin 程式碼片段
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
「狀態最佳化和撤銷」影片 16:49 處的 Kotlin 程式碼片段
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
「狀態最佳化和撤銷」影片中 18:35 處的 Kotlin 程式碼片段
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
「Compose 中的略過和穩定性」影片 21:07 處的 Kotlin 程式碼片段
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
「Compose 中的略過和穩定性」影片 21:53 處的 Kotlin 程式碼片段
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
「Fast composable design and performance tools」影片 29:40 處的殼層程式碼片段
android skills add jetpack-compose-performance