在一次項(xiàng)目開發(fā)中接觸到了jetpack Compose投放,并且還項(xiàng)目中在邏輯簡單的頁面,使用了compose去實(shí)現(xiàn)适贸。當(dāng)時(shí)覺得很新穎灸芳,實(shí)踐中也感覺到,這種響應(yīng)式的拜姿,與當(dāng)時(shí)的Vue/微信小程序/Flutter中思想大同小異烙样,可能是未來的一種原生寫UI的趨勢。在現(xiàn)在的每記和腳印項(xiàng)目中蕊肥,新實(shí)現(xiàn)的頁面谒获,都會優(yōu)先考慮用Compose去實(shí)現(xiàn)蛤肌。然而,Compose的一些性能優(yōu)化點(diǎn)及注意點(diǎn)批狱,也是做為開發(fā)人員需要熟悉的裸准,今天將做一個(gè)小的總結(jié)。
一赔硫、聲明式 vs 指令式編程
1炒俱、定義
無論是官網(wǎng)文檔還是介紹Compose的優(yōu)點(diǎn)時(shí),都會說到Compose是聲明式的爪膊。我們來回顧下权悟,在wiki上有著如下定義:
聲明式編程(英語:Declarative programming)或譯為聲明式編程,是對與命令式編程不同的編程范型的一種合稱推盛。它們建造計(jì)算機(jī)程序的結(jié)構(gòu)和元素峦阁,表達(dá)計(jì)算的邏輯而不用描述它的控制流程。
指令式編程(英語:Imperative programming)耘成;是一種描述電腦所需作出的行為的編程范型拇派。幾乎所有電腦的硬件都是指令式工作;幾乎所有電腦的硬件都是能執(zhí)行機(jī)器語言凿跳,而機(jī)器代碼是使用指令式的風(fēng)格來寫的。
通俗的來說就是:聲明式編程是一種把程序?qū)懗擅枋鼋Y(jié)果的形式疮方,而不是如何獲得結(jié)果的形式控嗜。它主要關(guān)注結(jié)果,而不是實(shí)現(xiàn)細(xì)節(jié)骡显。聲明式編程的代碼通常更簡潔疆栏,更容易理解和維護(hù)。
命令式編程則是一種把程序?qū)懗芍噶畹男问奖拱嬖V計(jì)算機(jī)如何實(shí)現(xiàn)結(jié)果壁顶。它更加關(guān)注細(xì)節(jié),如何實(shí)現(xiàn)任務(wù)溜歪。命令式編程的代碼通常更長若专,更難理解和維護(hù)。
2蝴猪、個(gè)人理解
Compose其實(shí)就是UI框架调衰,它最主要的功能就是讓開發(fā)人員更加快速的實(shí)現(xiàn) 頁面邏輯&交互效果 這是目的。
對于傳統(tǒng)的XML來說自阱,我們通過請求去服務(wù)器獲取數(shù)據(jù)嚎莉,請求成功后,我們需要findViewById
找到頁面元素View沛豌,再設(shè)置View的屬性趋箩,更新頁面展示狀態(tài)。整個(gè)過程是按 http請求 -> 響應(yīng) -> 尋找對應(yīng)View -> 更新對應(yīng)View
按部就班就地執(zhí)行,這種思想就是命令式編程叫确。
但是Compose描述為 http請求 -> 響應(yīng) -> 更新mutableData -> 引用對應(yīng)數(shù)據(jù)的View自動(dòng)重組
跳芳,整個(gè)過程不需要我們開發(fā)去寫更新UI的代碼(發(fā)出命令),而是數(shù)據(jù)發(fā)生改變启妹,UI界面自動(dòng)更新筛严,可以理解為聲明式。
二饶米、Compose優(yōu)勢
目前對于我的體驗(yàn)感受來說桨啃,Compose的優(yōu)勢體現(xiàn)在以下幾個(gè)點(diǎn):
頁面架構(gòu)清晰。對比以前mvp檬输,mvvm或結(jié)合viewbinding照瘾,少去了很多接口及編寫填充數(shù)據(jù)相關(guān)的代碼
動(dòng)畫API簡單好用。強(qiáng)大的動(dòng)畫支持丧慈,使得寫動(dòng)畫非常簡單析命。
開發(fā)效率高,寫UI速度快逃默,style鹃愤、shape等樣式使用簡單。
另外完域、還有一些官方優(yōu)勢介紹
三软吐、Compose 的重組作用域
雖然Compose 編譯器在背后做了大量工作來保證 recomposition 范圍盡可能小,我們還是需要對哪些情況發(fā)生了重組以及重組的范圍有一定的了解 吟税。
假設(shè)有如下代碼:
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Log.d(TAG, "Foo")
Button(onClick = {
text = "$text $text"
}.also { Log.d(TAG, "Button") }) {
Log.d(TAG, "Button content lambda")
Text(text).also { Log.d(TAG, "Text") }
}
}
其打印結(jié)果為:
D/Compose: Button content lambda
D/Compose: Text
按照開發(fā)經(jīng)驗(yàn)凹耙,第一感覺會是,text變量只被Text控件用到了肠仪。
分析一下肖抱,Button控件的定義為:
參數(shù) text
作為表達(dá)式執(zhí)行的調(diào)用處是 Button 的尾lambda,而后才作為參數(shù)傳入 Text()
异旧。 所以此時(shí)最小重組范圍是 Button 的 尾lambda 而非 Text()
另外還有兩點(diǎn)需要關(guān)注:
Compose 關(guān)心的是代碼塊中是否有對 state 的 read意述,而不是 write。
text 指向的 MutableState 實(shí)例是永遠(yuǎn)不會變的吮蛹,變的只是內(nèi)部的 value
重組中的 Inline 陷阱欲险!
非inline函數(shù) 才有資格成為重組的最小范圍,理解這點(diǎn)特別重要匹涮!
我們將代碼稍作改動(dòng)天试,為 Text()
包裹一個(gè) Box{...}
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Button(onClick = { text = "$text $text" }) {
Log.d(TAG, "Button content lambda")
Box {
Log.d(TAG, "Box")
Text(text).also { Log.d(TAG, "Text") }
}
}
}
日志如下:
D/Compose: Button content lambda
D/Compose: Box
D/Compose: Text
要點(diǎn):
-
Column
、Row
然低、Box
乃至Layout
這種容器類 Composable 都是inline
函數(shù)喜每,因此它們只能共享調(diào)用方的重組范圍务唐,也就是 Button 的 尾lambda
如果你希望通過縮小重組范圍提高性能怎么辦?
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Button(onClick = { text = "$text $text" }) {
Log.d(TAG, "Button content lambda")
Wrapper {
Text(text).also { Log.d(TAG, "Text") }
}
}
}
@Composable
fun Wrapper(content: @Composable () -> Unit) {
Log.d(TAG, "Wrapper recomposing")
Box {
Log.d(TAG, "Box")
content()
}
}
- 自定義非 inline 函數(shù)带兜,使之滿足 Compose 重組范圍最小化條件枫笛。
四、Compose開發(fā)時(shí)刚照,提高性能的關(guān)注點(diǎn)
當(dāng) Compose 更新重組時(shí)刑巧,它會經(jīng)歷三個(gè)階段(跟傳統(tǒng)View比較類似):
組合:Compose 確定要顯示的內(nèi)容 - 運(yùn)行可組合函數(shù)并構(gòu)建界面樹。
布局:Compose 確定界面樹中每個(gè)元素的尺寸和位置无畔。
繪圖:Compose 實(shí)際渲染各個(gè)界面元素啊楚。
基于這3個(gè)階段, 盡可能從可組合函數(shù)中移除計(jì)算浑彰。每當(dāng)界面發(fā)生變化時(shí)恭理,都可能需要重新運(yùn)行可組合函數(shù);可能對于動(dòng)畫的每一幀郭变,都會重新執(zhí)行您在可組合函數(shù)中放置的所有代碼颜价。
1、合理使用 remember
它的作用是:
- 保存重組時(shí)的狀態(tài)诉濒,并可以有重組后取出之前的狀態(tài)
引用官方的栗子??:
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
LazyColumn(modifier) {
// DON’T DO THIS
items(contacts.sortedWith(comparator)) { contact ->
// ...
}
}
}
-
LazyColumn
在滑動(dòng)時(shí)周伦,會使自身狀態(tài)發(fā)生改變導(dǎo)致ContactList
重組,從而contacts.sortedWith(comparator)
也會重復(fù)執(zhí)行未荒。而排序是一個(gè)占用CPU算力的函數(shù)横辆,對性能產(chǎn)生了較大的影響。
正確做法:
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
val sortedContacts = remember(contacts, sortComparator) {
contacts.sortedWith(sortComparator)
}
LazyColumn(modifier) {
items(sortedContacts) {
// ...
}
}
}
使用
remember
會對排序的結(jié)果進(jìn)行保存茄猫,使得下次重組時(shí),只要contacts
不發(fā)生變化 困肩,其值可以重復(fù)使用划纽。也就是說,它只進(jìn)行了一次排序操作锌畸,避免了每次重組時(shí)都進(jìn)行了計(jì)算勇劣。
提示:
- 更優(yōu)的做法是將這類計(jì)算的操作移出Compose方法,放到ViewModel中潭枣,再使用
collectAsState
或LanchEffect
等方式進(jìn)行觀測自動(dòng)重組比默。
2、使用LazyColumn盆犁、LazyRow
等列表組件時(shí)命咐,指定key
如下一段代碼,是一個(gè)很常見的需求(from官網(wǎng)):
??NoteRow記錄每項(xiàng)記錄的簡要信息谐岁,當(dāng)我們進(jìn)入編輯頁進(jìn)行修改后醋奠,需要將最近修改的一條按修改時(shí)間放到列表最前面榛臼。這時(shí),假若不指定每項(xiàng)Item的Key窜司,其中一項(xiàng)發(fā)生了位置變化沛善,都會導(dǎo)致其他的NoteRow
發(fā)生重組,然而我們修改的只是其中一項(xiàng)塞祈,進(jìn)行了不必要的渲染金刁。
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes
) { note ->
NoteRow(note)
}
}
}
正確的做法:
- 為每項(xiàng)Item提供 項(xiàng)鍵,就可避免其他未修改的NoteRow只需挪動(dòng)位置议薪,避免發(fā)生重組
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes,
key = { note ->
// 為每項(xiàng)Item提供穩(wěn)定的尤蛮、不會發(fā)生改變的唯一值(通常為項(xiàng)ID)
note.id
}
) { note ->
NoteRow(note)
}
}
}
3、使用 derivedStateOf
限制重組
??假設(shè)我們需要根據(jù)列表的第一項(xiàng)是否可見來決定劃到頂部的按鈕是否可見笙蒙,代碼如下:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
- 由于列表的滑動(dòng)會使
listState
狀態(tài)改變抵屿,而使用showButton
的AnimatedVisibility
會不斷重組,導(dǎo)致性能下降捅位。
??解決方案是使用派生狀態(tài)轧葛。如下 :
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
- 派生狀態(tài),可以這樣理解艇搀,只有在
derivedStateOf
里的狀態(tài)發(fā)生改變時(shí)尿扯,只關(guān)注和派發(fā)對UI界面產(chǎn)生了影響的狀態(tài)。這樣AnimatedVisibility
只會在改變時(shí)發(fā)生重組焰雕。對應(yīng)的應(yīng)用場景是衷笋,狀態(tài)發(fā)生了改變,但是我們只關(guān)注對界面產(chǎn)生了影響的狀態(tài)進(jìn)行分發(fā)矩屁,這種情況下辟宗,就可以考慮使用。
4吝秕、盡可能延遲State的讀行為
之前我們提到泊脐,對于一個(gè)Compose頁面來說,它會經(jīng)歷以下步驟:
第一步烁峭,Composition容客,這其實(shí)就代表了我們的Composable函數(shù)執(zhí)行的過程。
第二步约郁,Layout缩挑,這跟我們View體系的Layout類似,但總體的分發(fā)流程是存在一些差異的鬓梅。
第三步供置,Draw,也就是繪制绽快,Compose的UI元素最終會繪制在Android的Canvas上士袄。由此可見悲关,Jetpack Compose雖然是全新的UI框架,但它的底層并沒有脫離Android的范疇娄柳。
最后寓辱,Recomposition,也就是重組赤拒,并且重復(fù)1秫筏、2、3步驟挎挖。
盡可能推遲狀態(tài)讀取的原因这敬,其實(shí)還是希望我們可以在某些場景下直接跳過Recomposition的階段、甚至Layout的階段蕉朵,只影響到Draw崔涂。
??分析如下代碼:
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) { Start
val scroll = rememberScrollState(0)
// ...
Title(snack, scroll.value) // 1,狀態(tài)讀取
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scroll: Int) {
// ...
val offset = with(LocalDensity.current) { scroll.toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2始衅,狀態(tài)使用
) {
// ...
}
}
上面的代碼有兩個(gè)注釋冷蚂,注釋1,代表了狀態(tài)的讀妊凑ⅰ蝙茶;注釋2,代表了狀態(tài)的使用诸老。這種“狀態(tài)讀取與使用位置不一致”的現(xiàn)象隆夯,其實(shí)就為Compose提供了性能優(yōu)化的空間。
那么别伏,具體我們該如何優(yōu)化呢蹄衷?簡單來說,就是讓:“狀態(tài)讀取與使用位置一致”厘肮。
改為如下 :
// 代碼段12
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) {
val scroll = rememberScrollState(0)
// ...
Title(snack) { scroll.value } // 1愧口,Laziness
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
// ...
val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2,狀態(tài)讀取+使用
) {
// ...
}
}
理解: 由于我們將scroll.value變成了Lambda轴脐,所以,它并不會在composition期間產(chǎn)生狀態(tài)讀取行為抡砂,這樣大咱,當(dāng)scroll.value發(fā)生變化的時(shí)候,就不會觸發(fā)「重組」注益,這就是 延遲 的意義碴巾。
五、小結(jié)
其實(shí)以上案例優(yōu)化的點(diǎn)在本質(zhì)上丑搔,都是在踐行:狀態(tài)讀取與使用位置一致的原則厦瓢。但是需要我們對Compose的底層原理提揍,快照系統(tǒng),還有ScopeUpdateScope有一定的了解煮仇。這樣才會讓我們有著深刻的理解劳跃,代碼為什么要這么寫。