Jetpack Compose(4)——重組
上一篇文章講了 Compose 中狀態(tài)管理的基礎(chǔ)知識,本文講解 Compose 中狀重組的相關(guān)知識。
一、狀態(tài)變化
1.1 狀態(tài)變化是什么
根據(jù)上篇文章的講解,在 Compose 我們使用 State 來聲明一個(gè)狀態(tài),當(dāng)狀態(tài)發(fā)生變化時(shí),則會觸發(fā)重組。那么狀態(tài)變化是指什么呢?
下面我們來看一個(gè)例子:
@Composable
fun NumList() {
val num by remember {
mutableStateOf(mutableListOf(1, 2, 3))
}
Column {
Button(onClick = {
num += (num.last() + 1)
Log.d("sharpcj", "num: $num")
}) {
Text(text = "click to add one")
}
num.forEach {
Text(text = "item --> $it")
}
}
}
這段代碼中,我們定義了一個(gè) State ,其包裹的類型是 MutableList, 并且每次點(diǎn)擊,我們就給該 mutableList 增加一個(gè)元素。運(yùn)行一下:

我們點(diǎn)擊了按鈕,界面并沒有發(fā)生變化,但是,從日志看到,每次點(diǎn)擊后,list 中的元素的確增加了一個(gè)。
2024-03-18 20:51:41.472 12574-12574 sharpcj com.sharpcj.hellocompose D num: [1, 2, 3, 4]
2024-03-18 20:51:42.411 12574-12574 sharpcj com.sharpcj.hellocompose D num: [1, 2, 3, 4, 5]
2024-03-18 20:51:43.347 12574-12574 sharpcj com.sharpcj.hellocompose D num: [1, 2, 3, 4, 5, 6]
原因是什么呢?其實(shí)狀態(tài)發(fā)生變化,實(shí)際上指的是 State 包裹的對象,進(jìn)行 equals 比較,如果不相等,則認(rèn)為狀態(tài)變化,否則認(rèn)為沒有發(fā)生變化。所以這里就解釋得通了,我們雖然在點(diǎn)擊按鈕后,給 mutableList 增加了元素,但是 mutableList 在進(jìn)行前后比較時(shí),比較的是其引用,對象的引用并沒有發(fā)生變化,所以沒有發(fā)生重組。【這里結(jié)論并不準(zhǔn)確,下面穩(wěn)定類型詳細(xì)解釋說】
那為了讓其發(fā)生重組,我們稍作修改,每次點(diǎn)擊按鈕時(shí),創(chuàng)建一個(gè)新的 list,然后賦值,看看是不是我們所期待的結(jié)果。
@Composable
fun NumList() {
var num by remember {
mutableStateOf(mutableListOf(1, 2, 3))
}
Column {
Button(onClick = {
val num1 = num.toMutableList()
num1 += (num1.last() + 1)
num = num1
Log.d("sharpcj", "num: $num")
}) {
Text(text = "click to add one")
}
num.forEach {
Text(text = "item --> $it")
}
}
}
再次運(yùn)行程序:

結(jié)果符合我們的預(yù)期。那對于 List 類型的數(shù)據(jù)對象,每次狀態(tài)發(fā)生變化,我們創(chuàng)建了一個(gè)新對象,這樣在進(jìn)行 equals 比較時(shí),必定不相等,則會觸發(fā)重組。
1.2 mutableStateListOf 和 mutableStateMapOf
上面的問題,我們雖然接解決了, 但是寫法不夠優(yōu)雅,其實(shí) Compose 給我們提供了一個(gè)函數(shù) mutableStateListOf 來解決這類問題,我們看看這個(gè)函數(shù)怎么用,改寫上面的例子
@Composable
fun NumList() {
val num = remember {
mutableStateListOf(1, 2, 3)
}
Column {
Button(onClick = {
num += (num.last() + 1)
Log.d("sharpcj", "num: $num")
}) {
Text(text = "click to add one")
}
num.forEach {
Text(text = "item --> $it")
}
}
}
這樣就可以滿足我們的需求。 mutableStateListOf 返回了一個(gè)可感知內(nèi)部數(shù)據(jù)變化的 SnapshotStateList<T>, 它的內(nèi)部的實(shí)現(xiàn)為了保證不變性,仍然是拷貝元素,只不過它用了更加高效的實(shí)現(xiàn),比我們單純用toMutableList要高效得多。
由于 SnapshotStateList 繼承了 MutableList 接口,使得 MutableList 中定義的方法,依然可以使用。
同理,對于 Map 類型的對象, Compose 中提供了 mutableStateMapOf 方法,可以更優(yōu)雅,更高效地進(jìn)行處理。
思考如下問題:
假如我定義了一個(gè)類型:data class Hero(var name: String, var age: Int), 然后使用 mutableStateListOf 定義了狀態(tài),其中的元素是自定義的類型 Hero, 當(dāng)改變 Hero 的屬性時(shí), 與該狀態(tài)相關(guān)的 Composable 是否會發(fā)生重組?
data class Hero(var name: String, var age: Int)
@Composable
fun HeroInfo() {
val heroList = remember {
mutableStateListOf(Hero(name = "安其拉", age = 18), Hero(name = "魯班", age = 19))
}
Column {
Button(onClick = {
heroList[0].name = "DaJi"
heroList[0].age = 22
}) {
Text(text = "test click")
}
heroList.forEach {
Text(text = "student, name: ${it.name}, age: ${it.age} ")
}
}
}
二、重組的特性
2.1 Composable 重組是智能的
傳統(tǒng) View 體系通過修改 View 的私有屬性來改變 UI, Compose 則通過重組刷新 UI。 Compose 的重組非常“智能”,當(dāng)重組發(fā)生時(shí),只有狀態(tài)發(fā)生更新的 Composable 才會參與重組,沒有變化的 Composable 會跳過本次重組。
@Composable
fun KingHonour() {
Column {
var name by remember {
mutableStateOf("周瑜")
}
Button(onClick = {
name = "小喬"
}) {
Text(text = "改名")
}
Text(text = "魯班")
Text(text = name)
}
}
該例子中,點(diǎn)擊按鈕,改變了 name 的值,觸發(fā)重組,Button 和 Text(text = "魯班"),并不依賴該狀態(tài),雖然在重組時(shí)被調(diào)用了,但是在運(yùn)行時(shí)并不會真正的執(zhí)行。因?yàn)槠鋮?shù)沒有變化,Compose 編譯器會在編譯器插入相關(guān)的比較代碼。只有最后一個(gè) Text 依賴該狀態(tài),會參與真正的重組。
2.2 Composable 會以任意順序執(zhí)行
@Composable
fun Navi() {
Box {
FirstScreen()
SecondScreen()
ThirdScreen()
}
}
在代碼中出現(xiàn)多個(gè) Composable 函數(shù)時(shí),它們并不一定按照在代碼中出現(xiàn)的順序執(zhí)行,比如在一個(gè) Box 中,處于前景的 UI 具有較高優(yōu)先級。所以不要試圖通過外部變量與其它 Composable 產(chǎn)生關(guān)聯(lián)。
2.3 Composable 會并發(fā)執(zhí)行
重組中的 Composable 并不一定執(zhí)行在 UI 線程,它們可以在后臺線程中并發(fā)執(zhí)行,這樣利于發(fā)揮多喝處理器的優(yōu)勢。正因?yàn)榇耍残枰紤]線程安全問題。
2.4 Composable 會反復(fù)執(zhí)行
除了重組會造成 Composable 的再次執(zhí)行外,在動畫等場景中每一幀的變化都可能引起 Composable 的執(zhí)行。因此 Composable 可能在短時(shí)間內(nèi)多次執(zhí)行。
2.5 Composable 的執(zhí)行是“樂觀”的
所謂“樂觀”是指 Composable 最終會依據(jù)最新的狀態(tài)正確地完成重組。在某些場景下,狀態(tài)可能會連續(xù)變化,可能會導(dǎo)致中間態(tài)的重組在執(zhí)行時(shí)被打斷,新的重組會插進(jìn)來,對于被打斷的重組,Compose 不會將執(zhí)行一半的結(jié)果反應(yīng)到視圖樹上,因?yàn)樽詈笠淮蔚臓顟B(tài)總歸是正確的。
三、重組范圍
原則:重組范圍最小化。
只有受到了 State 變化影響的代碼塊,才會參與到重組,不依賴 State 變化的代碼則不參與重組。
如何確定重組范圍呢?修改上面的例子:
@Composable
fun RecompositionTest() {
Column {
Box {
Log.i("sharpcj", "RecompositionTest - 1")
Column {
Log.i("sharpcj", "RecompositionTest - 2")
var name by remember {
mutableStateOf("周瑜")
}
Button(onClick = {
name = "小喬"
}) {
Log.i("sharpcj", "RecompositionTest - 3")
Text(text = "改名")
}
Text(text = "魯班")
Text(text = name)
}
}
Box {
Log.i("sharpcj", "RecompositionTest - 4")
}
Card {
Log.i("sharpcj", "RecompositionTest - 5")
}
}
}
運(yùn)行,第一次我們看到,打印了如下日志:
2024-03-22 15:36:15.303 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 1
2024-03-22 15:36:15.305 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 2
2024-03-22 15:36:15.326 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 3
2024-03-22 15:36:15.337 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 4
2024-03-22 15:36:15.344 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 5
這是正常的,每個(gè)控件范圍內(nèi)都執(zhí)行了。我們點(diǎn)擊,button, 改變了 name 狀態(tài)。打印如下日志:
2024-03-22 15:37:48.480 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 1
2024-03-22 15:37:48.480 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 2
2024-03-22 15:37:48.491 19870-19870 sharpcj com.sharpcj.hellocompose I RecompositionTest - 4
首先我們 name 這個(gè)狀態(tài)影響的組件時(shí) Text,它所在的作用域應(yīng)該是 Column 內(nèi)部。打印 RecompositionTest - 2 好理解,可為什么連 Column 的上一級作用域 Box 也被調(diào)用了,并且連該 Box 的統(tǒng)計(jì) Box 也被調(diào)用了,但是 Card 卻又沒有被調(diào)用。這個(gè)好像與上面說的原則相悖。其實(shí)不然,我們看看 Column、Box、Card 源碼就清楚了。
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
Layout(
content = { BoxScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
@Composable
fun Card(
modifier: Modifier = Modifier,
shape: Shape = CardDefaults.shape,
colors: CardColors = CardDefaults.cardColors(),
elevation: CardElevation = CardDefaults.cardElevation(),
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit
) {
Surface(
modifier = modifier,
shape = shape,
color = colors.containerColor(enabled = true),
contentColor = colors.contentColor(enabled = true),
tonalElevation = elevation.tonalElevation(enabled = true),
shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
border = border,
) {
Column(content = content)
}
}
不難發(fā)現(xiàn), Column 和 Box 都是使用 inline 修飾的。
最后簡單了解下 Compose 重組的底層原理。
經(jīng)過 Compose 編譯器處理后的 Composable 代碼在對 State 進(jìn)行讀取時(shí),能夠自動建立關(guān)聯(lián),在運(yùn)行過程中,當(dāng) State 變化時(shí), Compose 會找到關(guān)聯(lián)的代碼塊標(biāo)記為 Invalid, 在下一渲染幀到來之前,Compose 觸發(fā)重組并執(zhí)行 invalid 代碼塊, invalid 代碼塊即下一次重組的范圍。能夠被標(biāo)記為 Invalid 的代碼必須是非 inline 且無返回值的 Composable 函數(shù)或 lambda。
需要注意的是,重組的范圍,與只能跳過并不沖突,確定了重組范圍,會調(diào)用對應(yīng)的組件代碼,但是當(dāng)參數(shù)沒有變化時(shí),在運(yùn)行時(shí)不會真正執(zhí)行,會跳過本次重組。
四、參數(shù)類型的穩(wěn)定性
4.1 穩(wěn)定和不穩(wěn)定
前面,Composable 狀態(tài)變化觸發(fā)重組,狀態(tài)變化基于 equals 比較結(jié)果,這是不準(zhǔn)確的。準(zhǔn)確地說:只有當(dāng)比較的狀態(tài)對象,是穩(wěn)定的,才能通過 equals 比較結(jié)果確定是否重組。什么叫穩(wěn)定的?還是看一個(gè)例子:
data class Hero(var name: String)
val shangDan = Hero("呂布")
@Composable
fun StableTest() {
var greeting by remember {
mutableStateOf("hello, 魯班")
}
Column {
Log.i("sharpcj", "invoke --> 1")
Text(text = greeting)
Button(onClick = {
greeting = "hello, 魯班大師"
}) {
Text(text = "搞錯(cuò)了,是魯班大師")
}
ShangDan(shangDan)
}
}
@Composable
fun ShangDan(hero: Hero) {
Log.i("sharpcj", "invoke --> 2")
Text(text = hero.name)
}
運(yùn)行一下,打印
2024-03-22 17:07:50.248 26973-26973 sharpcj com.sharpcj.hellocompose I invoke --> 1
2024-03-22 17:07:50.272 26973-26973 sharpcj com.sharpcj.hellocompose I invoke --> 2
點(diǎn)擊 Button,再次看到打印:
2024-03-22 17:07:53.182 26973-26973 sharpcj com.sharpcj.hellocompose I invoke --> 1
2024-03-22 17:07:53.191 26973-26973 sharpcj com.sharpcj.hellocompose I invoke --> 2
問題來了, Shangdan 這個(gè)組件依賴的只依賴一個(gè)參數(shù),并且參數(shù)也沒有改變,為什么確在重組過程中被調(diào)用了呢?
接下來,我們將 Hero 這個(gè)類做點(diǎn)改變,將其屬性聲明由 var 變成 val:
data class Hero(val name: String)
再次運(yùn)行,
2024-03-22 17:35:41.435 28561-28561 sharpcj com.sharpcj.hellocompose I invoke --> 1
2024-03-22 17:35:41.458 28561-28561 sharpcj com.sharpcj.hellocompose I invoke --> 2
點(diǎn)擊button:
2024-03-22 17:35:47.790 28561-28561 sharpcj com.sharpcj.hellocompose I invoke --> 1
這次,Shangdan 這個(gè) Composable 沒有參與重組了。為什么會這樣呢?
其實(shí)是在因?yàn)榇饲埃?var 聲明 Hero 類的屬性時(shí),Hero 類被 Compose 編譯器認(rèn)為是不穩(wěn)定類型。即有可能,我們傳入的參數(shù)引用沒有變化,但是屬性被修改過了,而 UI 又確實(shí)需要顯示修改后的最新值。而當(dāng)用 val 聲明屬性了,Compose 編譯器認(rèn)為該對象,只要對象引用不要變,那么這個(gè)對象就不會發(fā)生變化,自然 UI 也就不會發(fā)生變化,所以就跳過了這次重組。
常用的基本數(shù)據(jù)類型以及函數(shù)類型(lambda)都可以稱得上是穩(wěn)定類型,它們都不可變。反之,如果狀態(tài)是可變的,那么比較 equals 結(jié)果將不再可信。在遇到不穩(wěn)定類型時(shí),Compose 的抉擇是寧愿犧牲一些性能,也總好過顯示錯(cuò)誤的 UI。
4.2 @Stable 和 @Immutable
上面講了穩(wěn)定與不穩(wěn)定的概念,然而實(shí)際開發(fā)中,我們經(jīng)常會根據(jù)業(yè)務(wù)自定義 data class, 難道用了 Compose, 雖然 Kotlin 編碼規(guī)范,強(qiáng)調(diào)盡量使用 val, 但是還是要根據(jù)實(shí)際業(yè)務(wù),使用 var 來定義可變屬性。對于這種類型,我們可以為其添加 @Stable 注解,讓編譯器將其視為穩(wěn)定類型。從而發(fā)揮智能重組的作用,提升重組的性能。
@Stable
data class Hero(var name: String)
這樣,Hero 即便使用 var 聲明屬性,它作為參數(shù)傳入 Composable 中,只要對象引用沒變,都不會觸發(fā)重組。所以具體什么時(shí)候使用該注解,還需要根據(jù)需求靈活使用。
除了 @Stable,Compose 還提供了另一個(gè)類似的注解 @Immutable,與 @Stable 不同的是,@Immutable 用來修飾的類型應(yīng)該是完全不可變的。而 @Stable 可以用在函數(shù)、屬性等更多場景。使用起來更加方便,由于功能疊加,未來 @Immutable 有可能會被移除,建議優(yōu)先使用 @Stable。
最后總結(jié)一下:本文接著上篇文章的狀態(tài),講解了重組的一些特性,如何確定重組的范圍,以及重組的中的類型穩(wěn)定性概念,以及如何提升非穩(wěn)定類型在重組過程中的性能。
下一篇文章將會講解 Composable 的生命周期以及重組的副作用函數(shù)。

浙公網(wǎng)安備 33010602011771號