作者:古哥E下
如果一直關(guān)注 Compose 的發(fā)展的話,可以明顯感受到 2022 年和 2023 年的 Compose 使用討論的聲音已經(jīng)完全不一樣了, 2022 年還多是觀望,2023 年就有很多團(tuán)隊(duì)開始采納 Compose 來進(jìn)行開發(fā)了。不過也有很多同學(xué)接觸了下 Compose,然后就放棄了。要么使用起來賊特么不順手,要么就是感覺性能不行,卡。其實(shí),問題只是大家的思維沒有轉(zhuǎn)換過來,還不會(huì)寫 Compose。
為何要選擇 Compose
很多 Android 開發(fā)都會(huì)問:View 已經(jīng)這么成熟了,為何我要引入 Compose?
歷史也總是驚人的相似,React 橫空出世時(shí),很多前端同學(xué)也會(huì)問:jQuery 已經(jīng)如此強(qiáng)大了,為何要引入 JSX、Virtual DOM?
爭(zhēng)論總是無效的,時(shí)間會(huì)慢慢證明誰才會(huì)成為真正的主宰。
現(xiàn)在的前端同學(xué),可能連 jQuery 是什么都不知道了。其作為曾經(jīng)前端的主宰,何其強(qiáng)大,卻也經(jīng)受不住來自 React 的降維打擊?;乜催@端歷史,那我們選擇 Compose 就顯得很自然了。
另一個(gè)大趨勢(shì)是 Kotlin 跨平臺(tái)的逐漸興起與成熟,也會(huì)推動(dòng) Compose 成為 Fultter 之外的選擇,而且可以不用學(xué)習(xí)那除了寫 Flutter 就完全沒用的 Dart 語言。
但是,我也不推薦大家隨隨便便就把 Compose 接入的項(xiàng)目中。因?yàn)?,?guó)內(nèi)的開發(fā)現(xiàn)狀就是那樣,迭代速度要求快,但是也要追求穩(wěn)定。而接入 Compose 到使用 Compose 快速迭代,也是有一個(gè)痛苦的過程的,搞不好就要背鍋,現(xiàn)在這環(huán)境,背鍋可能就代表被裁了。
所以目前 Compose 依舊只能作為簡(jiǎn)歷亮點(diǎn)而非必備點(diǎn)??墒侨绻悴粚W(xué),萬一被要求是必備點(diǎn),那該怎么辦?
所以即使你不喜歡 Compose 這一套,那為了飯碗,該掌握的還是得掌握,畢竟市場(chǎng)飽和,我們是被挑選的哪一方。
Compose 的思想
聲明式 UI
Compose 的思想與 React、View、Fultter、SwiftUI 都是一脈相傳,那就是數(shù)據(jù)驅(qū)動(dòng) UI 與 聲明式 UI。以前的 View 體系,我們稱它為命令 UI。
命令式 UI 是我們拿到 View 的句柄,然后通過執(zhí)行命令,主動(dòng)更新它的的顏色、文字等等
聲明式 UI 則是我們構(gòu)建一個(gè)狀態(tài)機(jī),描述各個(gè)狀態(tài)下 UI 是個(gè)什么樣子的。
那些寫 Compose 怎么都不順手的童鞋,就是總想拿 View 的句柄,但又拿不到,所以就很痛苦,但如果轉(zhuǎn)換到狀態(tài)機(jī)的思維上,去定義各種情景的狀態(tài),那寫起來就非常舒服了。
Compose 從 View 體系進(jìn)化的點(diǎn)就是它貼近于真實(shí)的 UI 世界。因?yàn)槊總€(gè)界面就是一個(gè)復(fù)雜的狀態(tài)機(jī),以往我們命令式的操作,我們依舊要定義一套狀態(tài)系統(tǒng),某種狀態(tài)更新為某種 UI,有時(shí)候處理得不好,還會(huì)出現(xiàn)狀態(tài)錯(cuò)亂的問題。 Compose 則強(qiáng)制我們要思考 UI 的狀態(tài)機(jī)該是怎樣子的。
Virtual DOM
在 Compose 的世界中,是沒有介紹 Virtual DOM 這一概念的,但我覺得理解 Virtual DOM 能夠幫助我們更好的理解 Compose。 Virtual DOM 的誕生,一個(gè)原因是因?yàn)?DOM/View 節(jié)點(diǎn)實(shí)在是太重了,所以我們不能在數(shù)據(jù)變更時(shí)刪除這個(gè)節(jié)點(diǎn)再重新創(chuàng)建,我們也不沒有辦法通過 diff 的方式去追蹤到底發(fā)生了哪些變更。但大佬們的思維就比較活躍,因?yàn)殚_發(fā)過程中關(guān)注的一個(gè) DOM/ View 的屬性是很少的,所以就創(chuàng)造了一個(gè)輕量級(jí)的數(shù)據(jù)結(jié)構(gòu)來表示一個(gè) DOM/View 節(jié)點(diǎn),由于數(shù)據(jù)結(jié)構(gòu)比較輕量,那么銷毀創(chuàng)建就可以隨意點(diǎn)。每次更新狀態(tài),我可以用新狀態(tài)去創(chuàng)造一個(gè)新的 Virtual DOM Tree, 然后與舊的 Virtual DOM Tree 進(jìn)行 diff,然后將 diff 的結(jié)果更新到 DOM / View 上去, React Native 就是把前端的 DOM 變成移動(dòng)端的 View,因而開啟了 UI 跨平臺(tái)動(dòng)態(tài)化的大門。
那這和 Compose 有什么關(guān)系呢?我們可以認(rèn)為,Compose 的函數(shù)讓我們來生成 Virtual DOM 樹,Compose 內(nèi)部叫 SlotTable,框架用了全新的內(nèi)部結(jié)構(gòu)來代表 DOM 節(jié)點(diǎn)。每次我們狀態(tài)的變更,就會(huì)觸發(fā) Composable 函數(shù)重新執(zhí)行以生成新的 Virtual DOM,這個(gè)過程叫做 Recomposition。
所以重點(diǎn)來了,發(fā)生狀態(tài)更新后,框架會(huì)首先去重新生成 Virtual DOM 樹,交給底層去比對(duì)變更,最終渲染輸出。如果我們頻繁的變更狀態(tài),那就會(huì)頻繁的觸發(fā) Recomposition,如果每次還是重新生成一個(gè)巨大的 Virtual DOM 樹,那框架內(nèi)部的 diff 就會(huì)非常耗時(shí),那么性能問題隨之就來了,這是很多同學(xué)用 Compose 寫出的代碼卡頓的原因。
Compose 性能最佳實(shí)踐
如果我們有了 Virtual DOM 這一層認(rèn)識(shí),那么就能夠想到該怎樣去保持 Compose 的高性能了,那就是
1.減少 Composable 函數(shù)自身的計(jì)算
2.減小狀態(tài)變更的頻次
3.減小狀態(tài)變更的造成 Recomposition 的范圍以減小 diff 更新量
4.減小 Recomposition 時(shí)的變更量以減小 diff 更新量
減少 Composable 函數(shù)自身的計(jì)算
這個(gè)很好理解,如果 Recomposition 發(fā)生了,那么整個(gè)函數(shù)就會(huì)重新執(zhí)行,如果有復(fù)雜的計(jì)算邏輯,那就會(huì)造成函數(shù)本身的消耗很大,而解決措施也簡(jiǎn)單,就是通過 remember 緩存計(jì)算結(jié)果
@Composable
func Test(){
val ret = remember(arg1, arg2) { // 通過參數(shù)判斷是否要重新計(jì)算
// 復(fù)雜的計(jì)算邏輯
}
}
減少狀態(tài)變更的頻次
這個(gè)主要是減少無效的狀態(tài)變更,如果有多個(gè)狀態(tài),其每個(gè)狀態(tài)下的執(zhí)行結(jié)果是一樣的,那這些狀態(tài)間的變更就沒有意義了,應(yīng)該統(tǒng)一成唯一的狀態(tài)。
其實(shí)官方在 mutableStateOf 的入?yún)?policy 上已經(jīng)定制了幾種判斷狀態(tài)值是否變更的策略:
- StructuralEqualityPolicy: 通過值判等(==)的來看其是否發(fā)生變更
- ReferentialEqualityPolicy: 必須是同一個(gè)對(duì)象(===)才算未發(fā)生變更
- NeverEqualPolicy : 總是觸發(fā)狀態(tài)變更
默認(rèn)為 StructuralEqualityPolicy,也符合一般情況的要求。
除此之外,我們減小狀態(tài)變更頻率的手段就是 derivedStateOf。 它的用途主要是我們就是將多個(gè)狀態(tài)值收歸為統(tǒng)一的狀態(tài)值, 例如:
1.列表是否滾動(dòng)到了頂部,我們拿到的 scorllY 是很頻繁變更的值,但我們關(guān)注的只是 scorllY == 0
2.根據(jù)內(nèi)容為空判定發(fā)送按鈕是否可點(diǎn)擊,我們關(guān)注的是 input.isNotBlank()
3.多個(gè)輸入的聯(lián)合校驗(yàn)
4…
我們以發(fā)送按鈕為例:
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
// 使用 canSend
SendButton(canSend)
// 其它很多代碼
}
這樣子,我們可以多次更新 input 的值,但是只有當(dāng) canSend 發(fā)生變更時(shí)才會(huì)觸發(fā) Test 的 Recomposition。
減小狀態(tài)變更的造成 Recomposition 的范圍
Recomposition 是以函數(shù)為作用范圍的,所以某個(gè)狀態(tài)觸發(fā)了 Recomposition,那么這個(gè)函數(shù)就會(huì)重新執(zhí)行一次。但需要注意的是,不是狀態(tài)定義的函數(shù)執(zhí)行Recomposition,而是狀態(tài)讀取的函數(shù)會(huì)觸發(fā) Recomposition。
還是以上面的輸入的例子為例。 如果我在 Test 函數(shù)執(zhí)行期內(nèi)讀取了 input.value, 那么 input 變更時(shí)就會(huì)觸發(fā) Test 函數(shù)的重組。注意的是函數(shù)執(zhí)行期內(nèi)讀取,而不是函數(shù)代碼里寫了 input.value。上面 canSend 的 derivedStateOf 雖然也有調(diào)用 input.value,但因?yàn)樗且?lambda 的形式存在,不是會(huì)在執(zhí)行 Test 函數(shù)時(shí)就執(zhí)行,所以不會(huì)因?yàn)?input.value 變更就造成 Test 的 Recomposition。
但如果我在函數(shù)體內(nèi)使用 input.value,例如:
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
Text(input.value)
SendButton(canSend)
OtherCode(arg1, arg2)
OtherCode1(arg1, arg2)
}
那就會(huì)因?yàn)?input 的變更而造成 Test 的重組, canSend 使用 derivedStateOf 也就是做無用功了。更嚴(yán)重的是可能有很多其它與 input 無關(guān)的代碼也會(huì)再次執(zhí)行。
所以我們需要把狀態(tài)變更觸發(fā) Recomposition 的代碼用一個(gè)子組件來承載:
@Composable
func InputText(input: () -> String){
Text(input())
}
@Composable
func Test(){
val input = remember {
mutabtleStateOf('')
}
val canSend = remember {
derivedStateOf { input.value.isNotBlank() }
}
InputText {
input.value
}
SendButton(canSend)
OtherCode(arg1, arg2)
OtherCode1(arg1, arg2)
}
我們重新創(chuàng)建了一個(gè) InputText 函數(shù),然后通過 lambda 的形式傳遞 input,因而現(xiàn)在 input 變更造成的 Recomposition 就局限于 InputText 了,而其它的無關(guān)代碼就不會(huì)被執(zhí)行,這樣范圍就大大縮減了。
減小 Recomposition 時(shí)的變更量
加入我們的函數(shù) Recomposition 的范圍已經(jīng)沒辦法縮減了,例如上面 canSend 變更觸發(fā) Test 的 Recomposition,這造成 OtherCode 組件的重新執(zhí)行好像無法避免了。其實(shí)官方也想到了這種情況,所以它框架還會(huì)判斷 OtherCode 的參數(shù)是否發(fā)生了變更,依此來判斷 OtherCode 函數(shù)是否需要重新執(zhí)行。如果參數(shù)沒有變更,那么就可以開心的跳過它,那么 Recomposition 的變更量就大幅減小了。
那么怎么判斷參數(shù)沒有發(fā)生變更呢?如果是基礎(chǔ)類型和data class 等的數(shù)據(jù)結(jié)果還好,可以通過值判等的形式看其是否變更。但如果是列表或者自定義的數(shù)據(jù)結(jié)構(gòu)就麻煩了。 因?yàn)榭蚣軣o法知道其內(nèi)部是否發(fā)生了變更。
以 a: List 為例,雖然重組時(shí)我拿到的是同一個(gè)對(duì)象 a, 但其實(shí)現(xiàn)類可能是 ArraryList, 并且可能調(diào)用 add/remove 等方法變更了數(shù)據(jù)結(jié)構(gòu)。所以在保證正確性優(yōu)先的情況下,框架只得重新調(diào)用整個(gè)函數(shù)。
@Composable
fun SubTest(a: List<String>){
//...
}
@Composable
fun Test(){
val input = remember {
mutabtleStateOf('')
}
val a = remember {
mutableStateOf(ArrayList<String>())
}
// 因?yàn)樽x取了 input.value, 所以每次 input 變更,都會(huì)早成 Test 的 Recomposition
Test(input.value)
// 而因?yàn)?a 是個(gè) List,所以每次 SubTest 也會(huì)執(zhí)行 Recomposition
SubTest(a)
}
那要怎么規(guī)避這個(gè)問題呢? 那就是使用 kotlinx-collections-immutable 提供的 ImmutableList 等數(shù)據(jù)結(jié)構(gòu),如此就可以幫助框架正確的判斷數(shù)據(jù)是否發(fā)生了變更。
@Composable
fun SubTest(a: PersistentList<String>){
//...
}
@Composable
fun Test(){
val input = remember {
mutabtleStateOf('')
}
val a = remember {
mutableStateOf(persistentListOf<String>())
}
// 因?yàn)樽x取了 input.value, 所以每次 input 變更,都會(huì)早成 Test 的 Recomposition
Test(input.value)
// 而因?yàn)?a 是個(gè) List,所以每次 SubTest 也會(huì)執(zhí)行 Recomposition
SubTest(a)
}
而如果是我們自己定義的數(shù)據(jù)結(jié)構(gòu),如果是非 data class,那就要我們主動(dòng)加上 @Stable 注解,告訴框架這個(gè)數(shù)據(jù)結(jié)構(gòu)是不會(huì)發(fā)生變更,或者其變更我們都會(huì)用狀態(tài)機(jī)去處理的。特別需要注意的是使用 java 作為實(shí)體類而給 compose 使用的情況,那就是非常不友好了。
對(duì)于列表而言,我們往往需要用 for 循環(huán)或者 LazyColumn 之類的方式使用:
@Composable
fun SubTest(list: PersistentList<ItemData>){
for(item in list){
Item(item)
}
}
這個(gè)寫法,如果 list 不會(huì)變更,那也沒什么問題,可是如果列表發(fā)生了變更,例如原本是 12345, 我刪了一項(xiàng)變成 1345。
那么在 Recomposition 的時(shí)候,框架在比對(duì)變更時(shí),發(fā)現(xiàn)從第二項(xiàng)開始就全不同了,那么剩下的 Item 就得全部重新重組一次了,這也是非常耗費(fèi)性能的,所以框架提供了 key 的功能,通過它,框架可以檢測(cè)列表的 Item 移動(dòng)的情況。
@Composable
fun SubTest(list: PersistentList<ItemData>){
for(item in list){
key(item.id){
Item(item)
}
}
}
不過需要注意的是 key 需要具有唯一性。 LazyColumn 的 item 也有 key 的功能,其作用類似,其還有 contentType 的傳參,其作用和 RecyclerView 的多 itemType 類似,也是一個(gè)可以使用的優(yōu)化措施。
最后
Compose 業(yè)務(wù)上能做的優(yōu)化大體上就是這些了??傊覀兙褪俏覀円3纸M件的顆粒度盡可能的小,容易變動(dòng)的要獨(dú)立出來,非常穩(wěn)定的也要獨(dú)立出來,盡量使用 Immutable 的數(shù)據(jù)結(jié)構(gòu)。 如此之后, Compose 的流暢度還是非常不錯(cuò)的。
如果還覺得卡,那多半是因?yàn)槟闶褂玫氖?Debug 包,Compose 會(huì)在 Debug 包加很多調(diào)試信息,會(huì)很影響其流暢度的。切換到 Release 包,可能絲滑感就出來了。
為了幫助大家更好的熟知Jetpack Compose 這一套體系的知識(shí)點(diǎn),這里記錄比較全比較細(xì)致的《Jetpack 入門到精通》(內(nèi)含Compose) 學(xué)習(xí)筆記!??! 對(duì)Jetpose Compose這塊感興趣的小伙伴可以參考學(xué)習(xí)下……
Jetpack 全家桶(Compose)
Jetpack 部分
- Jetpack之Lifecycle
- Jetpack之ViewModel
- Jetpack之DataBinding
- Jetpack之Navigation
- Jetpack之LiveData
Compose 部分
1.Jetpack Compose入門詳解
2.Compose學(xué)習(xí)筆記
3.Compose 動(dòng)畫使用詳解文章來源:http://www.zghlxwxcb.cn/news/detail-580235.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-580235.html
到了這里,關(guān)于現(xiàn)代化 Android 開發(fā):Jetpack Compose 最佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!