前言
Jetpack Compose
的應用也逐漸廣泛起來即彪,由于聲明式UI
的特點烤低,Compose
在開發(fā)的易用性方面有較大優(yōu)勢刑顺,但相信很多人對于Compose
的性能問題有一些疑問柔纵。
這些問題有些是因為Compose
還是個新生事物缔杉,不夠成熟導致的,有些則是因為開發(fā)者的使用不當導致的搁料。
本文主要介紹如何編寫和配置應用程序以獲得最佳性能或详,并指出了一些要避免的問題。
正確配置應用
如果您的Compose
應用性能不佳郭计,則可能意味著存在配置問題霸琴。 首先應該檢查以下配置項
使用Release
模式構(gòu)建并且使用R8
如果您發(fā)現(xiàn)性能問題,請確保嘗試在Release
模式下運行您的應用昭伸。Debug
模式對于發(fā)現(xiàn)許多問題很有用梧乘,但它會帶來顯著的性能成本,并且很難發(fā)現(xiàn)可能影響性能的其他代碼的問題。
同時您還應該使用 R8
編譯器從您的應用程序中刪除不必要的代碼选调。 默認情況下夹供,在Release
模式下構(gòu)建會自動使用 R8
編譯器。
使用baseline profile
Compose
作為一個單獨的庫分發(fā)仁堪,而不是作為 Android
平臺的一部分哮洽。這種方法讓我們可以經(jīng)常更新 Compose
并支持較舊的 Android
版本。但是枝笨,將 Compose
作為庫分發(fā)也會產(chǎn)生一定的成本袁铐。 Android
平臺代碼已編譯并安裝在設備上。另一方面横浑,庫需要在應用程序啟動時加載剔桨,并在需要功能時及時解釋(即JIT
)。這可能會在啟動時減慢應用程序的速度徙融,并且每當它首次使用庫功能時洒缀。
您可以通過定義baseline profile
來提高性能。這些配置文件定義了用戶主流程所需的類和方法欺冀,并與您應用的 APK
一起分發(fā)树绩。在應用程序安裝期間,ART
會提前編譯該關鍵代碼(即AOT
)隐轩,以便在應用程序啟動時準備好使用饺饭。
定義一個好的baseline profile
并不總是那么容易,因此 Compose
默認附帶一個职车。因此默認情況下你不需要做任何額外工作瘫俊。
同時,如果您選擇定義自己的配置文件悴灵,您可能會生成一個實際上不會提高應用程序性能的配置文件扛芽。您應該測試配置文件以驗證它是否有幫助。一個很好的方法是為您的應用程序編寫 Macrobenchmark測試积瞒,并在您編寫和修改baseline profile
時檢查測試結(jié)果川尖。有關如何為 Compose UI
編寫 Macrobenchmark
測試的示例,請參閱 Macrobenchmark Compose示例茫孔。
總得來說叮喳,使用baseline profile
即通過AOT
取代JIT
,加快Compose
首次運行的速度缰贝。
在默認情況下Compose
已經(jīng)自帶了一個默認的baseline profile
嘲更,你不需要做什么額外工作就可以支持。
但如果你要自定義baseline profile
的話揩瞪,需要做好測試用例,驗證自定義的配置是否有效篓冲。
自定義baseline profile
比較麻煩李破,不過根據(jù)Google I/O
上給出的數(shù)據(jù)宠哄,可以達到20%到30%的啟動性能提升,大家可以根據(jù)情況決定是否使用
關于Compose
的一些最佳實踐
在編寫Compose
代碼時你可能會碰到一些常見的錯誤嗤攻。這些錯誤不影響運行毛嫉,但會損害您的 UI
性能。本節(jié)列出了一些最佳實踐來幫助您避免它們妇菱。
使用remember
減少計算
Compose
函數(shù)可以非常頻繁地運行承粤,就像動畫的每一幀一樣頻繁。 出于這個原因闯团,你應該盡可能少地在Compose
中做計算辛臊。
最常見的就是使用remember
, 這樣房交,計算只運行一次彻舰,并且可以在需要時獲取結(jié)果。
例如候味,這里有一些代碼顯示排序的名稱列表刃唤,其中排序操作比較耗時
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
LazyColumn(modifier) {
// DON’T DO THIS
items(contacts.sortedWith(comparator)) { contact ->
// ...
}
}
}
問題在于,每次重組 ContactsList
時白群,整個聯(lián)系人列表都會重新排序尚胞,即使列表沒有更改。 如果用戶滾動列表帜慢,只要出現(xiàn)新行笼裳,Composable
就會重新組合。
為了解決這個問題崖堤,在LazyColumn
之外對列表進行排序侍咱,并使用 remember
存儲排序后的列表:
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
val sortedContacts = remember(contacts, sortComparator) {
contacts.sortedWith(sortComparator)
}
LazyColumn(modifier) {
items(sortedContacts) {
// ...
}
}
}
如上,當?shù)谝淮谓M成 ContactList
時密幔,列表被排序一次楔脯。 如果聯(lián)系人或比較器更改,則重新生成排序列表胯甩。 否則昧廷,可組合項可以繼續(xù)使用緩存的排序列表。
當然:如果可能偎箫,最好將計算完全移到Compose
之外木柬,比如ViewModel
Lazy Layout
使用Key
Lazy Layout
使用智能重組,僅在必要時才會發(fā)生重組淹办。 同時眉枕,我們可以幫助它做出最佳決策。
假設用戶操作導致item
在列表中移動。 例如速挑,假設您顯示按修改時間排序的筆記列表谤牡,最近修改的筆記在最上面。
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes
) { note ->
NoteRow(note)
}
}
}
不過姥宝,這段代碼存在一定的問題翅萤。 假設底部的note
發(fā)生了變化。 它現(xiàn)在是最近修改的note
腊满,所以它應該排在列表的頂部套么,而其他每個note
都向下移動一個位置。
這里的問題是碳蛋,沒有您的幫助胚泌,Compose
不會意識到未更改的項目只是在列表中移動。 相反疮蹦,Compose
認為舊的“第 2 項”被刪除并創(chuàng)建了一個新的诸迟,依此類推,第 3 項愕乎、第 4 項一直如此阵苇。 結(jié)果是,Compose
會重新組合列表中的每一項感论,即使其中只有一項實際發(fā)生了變化绅项。
解決方案是提供item key
。 為每個item
提供一個穩(wěn)定的key
可以讓 Compose
避免不必要的重組比肄。 在這種情況下快耿,Compose
可以看到現(xiàn)在位于第 3 項和item
過去位于第 2 的item
相同。由于該項目的數(shù)據(jù)都沒有更改芳绩,因此 Compose
不必重新組合它掀亥。
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes,
key = { note ->
// Return a stable, unique key for the note
note.id
}
) { note ->
NoteRow(note)
}
}
}
使用 derivedStateOf
限制重組
在重組中使用狀態(tài)的一個風險是,如果狀態(tài)快速變化妥色,你的 UI
可能會比你預期的發(fā)生更多的重組搪花。
例如,假設您正在顯示一個可滾動的列表嘹害。 您檢查列表的狀態(tài)以查看哪個item
是列表中的第一個可見item
:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
這里的問題在于撮竿,如果用戶滾動列表,listState
會隨著用戶拖動手指而不斷變化笔呀。 這意味著該列表不斷被重新組合幢踏,而showButton
的結(jié)果也會被不斷計算。
但是许师,您只有在firstVisibleItemIndex
發(fā)生變化時才需要計算showButton
房蝉。 所以僚匆,這里多了很多額外的計算,會影響您的UI
性能
解決方案是使用derivedStateOf
搭幻。 derivedStateOf
告訴Compose
只有當我們關心的狀態(tài)發(fā)生變化時白热,才需要重組。
在這種情況下粗卜,當firstVisibleItemIndex
發(fā)生變化時才需要重組。但如果用戶還沒有滾動到足以將新item
帶到頂部的程度纳击,則不需要重新組合续扔。
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
注意,如果把狀態(tài)都放在ViewModel
里焕数,也就不用考慮這個了
盡可能延遲讀取State
您應該盡可能推遲讀取State
纱昧。 延遲讀取State
有助于確保 Compose
在重組時重新運行盡可能少的代碼。 例如堡赔,如果您的 UI
具有在composable
樹中高高提升的狀態(tài)识脆,并且您在子composable
中讀取狀態(tài),則可以將讀取的狀態(tài)包裝在 lambda
函數(shù)中善已。 這樣做會使讀取僅在實際需要時發(fā)生灼捂。
我們來看一段Jetsnack
在列表滾動時實現(xiàn)Title
折疊展開的代碼,為了達到這個效果换团,Title composable
需要知道滾動偏移量悉稠,以便使用修飾符來偏移自己。 在進行優(yōu)化之前艘包,這是Jetsnack
代碼的簡化版本:
@Composable
fun SnackDetail() {
// ...
Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)
// ...
Title(snack, scroll.value)
// ...
} // Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scroll: Int) {
// ...
val offset = with(LocalDensity.current) { scroll.toDp() }
Column(
modifier = Modifier
.offset(y = offset)
) {
// ...
}
}
當滾動狀態(tài)發(fā)生變化時的猛,Compose
會尋找最近的父重組作用域并使其無效。 在這種情況下想虎,最近的父級是可組合的是Box
卦尊。 因此 Compose
重組了 Box
,并且還重組了 Box
內(nèi)的任何可組合項舌厨。 如果您將代碼重構(gòu)為僅讀取您需要的State
岂却,那么您可以減少需要重組的元素數(shù)量。
@Composable
fun SnackDetail() {
// ...
Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)
// ...
Title(snack) { scroll.value }
// ...
} // Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
// ...
val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset)
) {
// ...
}
}
滾動參數(shù)現(xiàn)在是一個 lambda
邓线。 這意味著 Title
仍然可以引用被提升的狀態(tài)淌友,但該值只能在 Title
內(nèi)部讀取,也就是實際需要的地方骇陈。 這樣一來震庭,當滾動值發(fā)生變化時,最近的重組范圍現(xiàn)在是 Title composable
你雌,因此Compose
不再需要重組整個 Box
器联。
這是一個很好的改進二汛,但我們還可以做得更好。 因為我們所做的只是更改可組合標題的偏移量拨拓,這可以在布局階段完成肴颊,而不必經(jīng)過組合階段。
Compose
的階段
與大多數(shù)其他界面工具包一樣渣磷,Compose
會通過幾個不同的“階段”來渲染幀婿着。如果我們觀察一下 Android View
系統(tǒng),就會發(fā)現(xiàn)它有 3 個主要階段:測量醋界、布局和繪制竟宋。Compose
和它非常相似,但開頭多了一個叫做“組合”的重要階段形纺。
Compose
有 3 個主要階段:
- 組合:要顯示什么樣的界面丘侠。
Compose
運行可組合函數(shù)并創(chuàng)建界面說明。 - 布局:要放置界面的位置逐样。該階段包含兩個步驟:測量和放置蜗字。對于布局樹中的每個節(jié)點,布局元素都會根據(jù)
2D
坐標來測量并放置自己及其所有子元素脂新。 - 繪制:渲染的方式挪捕。界面元素會繪制到畫布(通常是設備屏幕)中。
優(yōu)化狀態(tài)讀取
知道了Compose
的3個階段戏羽,Compose
會執(zhí)行局部狀態(tài)讀取跟蹤担神,因此我們可以在適當階段讀取每個狀態(tài),從而盡可能降低需要執(zhí)行的工作量
如果我們在布局階段讀取狀態(tài)始花,就可以跳過組合階段妄讯,如果我們在繪制階段讀取狀態(tài),就可以跳過組合和布局
因此我們可以將offset
的讀取推遲到布局階段酷宵,這樣可以避免組合階段重新執(zhí)行
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
// ...
Column(
modifier = Modifier
.offset { IntOffset(y = scrollProvider()) }
) {
// ...
}
}
之前代碼使用了Modifier.offset(x: Dp, y: Dp)
亥贸,它以偏移量為參數(shù)。 通過切換到修飾符的 lambda
版本浇垦,您可以確保函數(shù)在布局階段讀取滾動狀態(tài)炕置。 因此,當滾動狀態(tài)發(fā)生變化時男韧,Compose
可以完全跳過組合階段朴摊,直接進入布局階段。 當您將頻繁更改的狀態(tài)變量傳遞給modifier
時此虑,應盡可能使用modifier
的 lambda
版本甚纲。
繪制階段讀取狀態(tài)的一個例子
上面我們看了一個在布局階段讀取狀態(tài)的例子,下面來看一下繪制階段讀取狀態(tài)的一個例子
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
這里盒子的背景顏色在兩種顏色之間快速切換朦前。 因此介杆,這種狀態(tài)變化非常頻繁鹃操。 然后可組合項在背景modifier
中讀取此狀態(tài)。 結(jié)果春哨,盒子必須在每一幀上重新組合荆隘,因為每一幀的顏色都在變化。
為了改善這一點赴背,我們可以使用基于 lambda
的modifier
:在本例中為 drawBehind
椰拒。這意味著僅在繪制階段讀取顏色狀態(tài)。
因此凰荚,Compose
可以完全跳過組合和布局階段耸三,當顏色發(fā)生變化時,Compose
會直接進入繪制階段浇揩。
避免反向?qū)懭?/h3>
Compose
有一個核心假設,即您永遠不會寫入已讀取的狀態(tài)憨颠。 當你這樣做時胳徽,它被稱為反向?qū)懭耄鼤е轮亟M在每一幀上發(fā)生爽彤,無休止养盗。
以下代碼顯示了此類錯誤的示例。
@Composable
fun BadComposable() {
var count by remember { mutableStateOf(0) }
// Causes recomposition on click
Button(onClick = { count++ }, Modifier.wrapContentSize()) {
Text("Recompose")
}
Text("$count")
count++ // Backwards write, writing to state after it has been read
}
這段代碼在讀取了狀態(tài)之后在可組合項末尾的更新了狀態(tài)适篙。 如果您運行此代碼往核,您會看到在單擊導致重組的按鈕后,隨著 Compose
重組此 Composable
嚷节,計數(shù)器會在無限循環(huán)中迅速增加聂儒,看到讀取的狀態(tài)已過期,因此會安排另一個重組 .
您可以通過從不在 Composition
中寫入狀態(tài)來完全避免向后寫入硫痰。 如果可能衩婚,請始終響應事件來更新狀態(tài),并使用 lambda
表達式效斑,就像前面的 onClick
示例一樣非春。
總結(jié)
本文主要介紹如何編寫和配置Compose
以獲得最佳性能,并指出了一些要避免的問題缓屠。