Compose 智能重組:編譯器視角下的黑科技

什么是智能重組
Android View 通過測量、布局和繪制三個階段完成 UI 渲染,Compose 整體上與 Android View 類似,但在開頭多了一個叫做“組合”的重要階段。在組合階段,Compose 會執行 @Composable 方法并輸出 UI 的樹狀結構與對應信息,為后續的布局階段提供數據基礎。
Compose 采用聲明式 UI 范式,不再像傳統 View 那樣通過調用 View 的 setXXX 方法來手動更新 UI,而是在 UI 狀態變更時再次執行組合、布局、繪制流程,以此完成 UI 的更新,重新組合的過程就叫做“重組“。
然而重組是一個比較重的過程,需要重新執行 @Composable 方法并更新內存中關于 UI 樹的信息,如果每一個狀態的變更都要走一遍整個流程將會帶來嚴重的性能問題。因此在 UI 狀態變化時,Compose 會智能的選擇必要的 @Composable 方法進行重組,并盡可能跳過不必要的代碼執行,這就是 Compose 的"智能重組"。
下面的代碼展示了一個簡單的重組過程,在 Column、Text 組件上設置了隨機的背景色,如果它們被重新組合那么背景色就會隨機變化,我們可以通過這個來判斷 UI 是否發生重組:
@Composable
fun RecomposeDemo() {
var count by remember { mutableStateOf(0) }
Column(Modifier.background(randomColor()).padding(20.dp)) {
RecomposeAwareText("Count: $count", Modifier.clickable {
count++
})
RecomposeAwareText("Static Text")
}
}
@Composable
fun RecomposeAwareText(text: String, modifier: Modifier = Modifier) {
Text(text, modifier.background(randomColor()).padding(20.dp))
}
fun randomColor(): Color {
val random = Random(System.currentTimeMillis())
return Color(
red = random.nextInt(256),
green = random.nextInt(256),
blue = random.nextInt(256),
alpha = 255
)
}運行效果如下圖所示,點擊第一個 Text 會觸發 count 變化,從而觸發 UI 的重組。從執行結果來看 Column 和第一個 Text 都發生了重組,而第二個 Text 并沒有重新執行。這也比較符合直覺,畢竟第二個 Text 的內容沒有發生變化,也就不應該重組。
然而重組的本質就是重新執行 @Composable 方法,從代碼邏輯上來說第一個 RecomposeAwareText 被執行的情況下,第二個 RecomposeAwareText 也理應被執行。但正是由于 Compose 的智能重組機制跳過了不必要的執行,從而避免了對第二個 Text 的重組。

智能重組機制由 Compose 編譯器和運行時協同完成,本文將聚焦于 Compose 編譯器在其中發揮的作用,徹底揭開智能重組背后的"黑科技"。
編譯器做了什么
為了實現智能重組能力,Compose 編譯器會在編譯期對每個 @Composable 方法進行轉換,插入額外的參數與控制邏輯。我們將從一個簡單的示例入手,初步了解編譯器到底做了哪些改動,建立整體認知。后續章節將逐步拆解各個關鍵環節,深入解析這些改動背后的設計原理。
在下面這個例子中,RecomposeDemo 讀取了 uiState 并將它的值傳遞給 ComposeUI,在 ComposeUI 中將參數 content 進行打印。
var uiState by mutableStateOf("UI State")
@Composable
fun RecomposeDemo() {
ComposeUI(uiState)
}
@Composable
fun ComposeUI(content: String) {
println(content)
}經過 Compose 編譯器編譯后的代碼如下(僅保留關鍵的部分):
@Composable
fun RecomposeDemo($composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup(1961523638)
// 判斷參數是否變化,如果沒有變化則不執行代碼
if ($changed != 0 || !$composer.skipping) {
ComposeUI(recordReadValue($readState, "uiState", uiState), $composer, 0)
} else {
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
// 為 RestartGroup 注冊 State 變更時的回調,重新觸發 RecomposeDemo 執行
RecomposeDemo($composer, updateChangedFlags($changed or 0b0001))
}
}
@Composable
fun ComposeUI(content: String, $composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup(-1501355475)
val $dirty = $changed
if ($changed and 0b0110 == 0) {
// 判斷 content 參數是否變化
$dirty = $dirty or if ($composer.changed(content)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
println(content)
} else {
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
ComposeUI(content, $composer, updateChangedFlags($changed or 0b0001))
}
}核心包含以下三部分變化:
1. 插入參數
Compose 編譯器在兩個 @Composable 方法上都增加了 $composer、$changed 兩個參數,$composer 可以看作是當前 Compose 的上下文環境,該參數會貫穿整個 Compose 組合階段,在 Composable 方法調用鏈上層層傳遞。$changed 參數則是用于提供當前方法參數變化信息,在方法內會結合該參數來判斷是否跳過當前 @Composable 方法的執行。
2. 插入重組邏輯
兩個 @Composable 方法的首尾都插入了 startRestartGroup 和 endRestartGroup 調用,這其實是創建了一個 RestartGroup,在這個 Group 內如果某個方法調用了 State.getValue 方法,那么這個 State 就會與當前的 RestartGroup 綁定,后續這個 State 變更時就會觸發該 RestartGroup 的執行,也就是觸發重組。
3. 跳過執行邏輯
在 ComposeUI 方法中,插入了 $dirty 變量以及對應的計算邏輯,該變量用于最終判斷當前方法入參 content 是否發生變化,并根據該變量來決定是否跳過 ComposeUI 內容的執行,這是智能重組的核心所在。
創建重組作用域
什么是重組作用域
通過前面對反編譯后代碼的分析,我們知道每個 Compose 方法都被包裝在一個名為 RestartGroup 的特殊結構中。當一個 Compose 方法執行時,它會啟動一個 RestartGroup。在這個 RestartGroup 的作用域內,如果讀取了任何 State,那么這個 State 就會與當前的 RestartGroup 建立關聯。當 Compose 方法執行完畢,這個 RestartGroup 也就隨之結束。
一旦后續這個 State 的值發生更新,Compose 就會自動觸發與該 State 關聯的 RestartGroup 進行重組。而這個 RestartGroup 所屬的 @Composable 方法,就是我們所說的重組作用域。
@Composable
fun RecomposeDemo($composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup(1961523638)
// 判斷參數是否變化,如果沒有變化則不執行代碼
if ($changed != 0 || !$composer.skipping) {
ComposeUI(recordReadValue($readState, "uiState", uiState), $composer, 0)
} else {
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
// 為 RestartGroup 注冊 State 變更時的回調,重新觸發 RecomposeDemo 執行
RecomposeDemo($composer, updateChangedFlags($changed or 0b0001))
}
}
@Composable
fun ComposeUI(content: String, $composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup(-1501355475)
val $dirty = $changed
if ($changed and 0b0110 == 0) {
// 判斷 content 參數是否變化
$dirty = $dirty or if ($composer.changed(content)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
println(content)
} else {
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
ComposeUI(content, $composer, updateChangedFlags($changed or 0b0001))
}
}還是以第二節的代碼為例子,RecomposeDemo 執行邏輯如下:
- RecomposeDemo 執行
- RecomposeDemo 啟動 RestartGroup
- 讀取 uiState
- 調用 ComposeUI
a. ComposeUI 啟動 RestartGroup
b. ComposeUI 結束 RestartGroup - RecomposeDemo 結束 RestartGroup
uiState 被讀取時處于 RecomposeDemo 的作用域內,所以后續 uiState 更新時將會觸發 RecomposeDemo 的重新執行。
哪些 Compose 方法沒有重組作用域
我們稱那些在編譯階段被 Compose 編譯器包裝進 RestartGroup 的方法是“可重啟”的,但我們需要明確一點:并非所有 Compose 方法都能被重啟。這意味著,簡單地認為“調用一個 Compose 方法就定義了一個重組作用域”是不準確的。
重組作用域的范圍會直接影響性能,而如果我們知道哪些 Composable 不能被重啟,就能寫出更合理的代碼。這樣一旦遇到性能問題,也會有明確的排查方向。
我們可以通過閱讀 Compose 編譯器源碼來了解哪些方法無法被重啟,具體邏輯在 ComposableFunctionBodyTransformer#shouldBeRestartable 中,代碼如下:

代碼注釋非常詳細,下面介紹其中比較重要的場景。
內聯方法
當一個函數被內聯后,它就不再擁有一個獨立的函數調用幀。它的代碼邏輯直接成為了調用函數的一部分,所以它就無法作為一個獨立的代碼塊進行重組。
被 @NonRestartableComposable 標記的方法
@NonRestartableComposable 是 Compose 提供的注解,允許開發者指定某個 Compose 方法不可重啟。一般用于優化簡單的 Compose 方法,這些方法內部僅僅是調用其他 @Compose 方法,這樣可以避免冗余的 RestartGroup 與邏輯處理。在 Compose 內部就有大量的場景使用。

有非 Unit 返回值的方法
如果一個 Compose 方法存在非 Unit 的返回值,那這個方法也不能夠被重啟。因為這種方法的返回值通常是被調用方依賴,如果某次重組只重啟了該方法,那么調用方將無法感知該方法的返回值變更,可能造成預期外的 UI 異常。
open 方法
open 方法也無法被重啟,因為這類方法被 override 后會生成新的 RestartGroup,那么就會在一個方法中出現兩個RestartGroup,重組時可能發生異常。
內聯方法的 Composable Lambda 參數
如果一個 Composable Lambda 是作為 inline 方法的參數,那么這個 Composable Lambda 也無法被重組。最常見的是 Column、Box 等布局組件,這些組件均為 inline 方法,且接受一個 Composable Lambda 作為參數。

在以下代碼中,uiState 關聯的重組作用域為 ComposeUI,而不是 Column 或 Column 的尾 Lambda。
var uiState by mutableStateOf("UI State")
@Composable
fun ComposeUI(content: String) {
Column {
println(uiState)
}
}如果在這種場景下希望 ComposeLambda 能夠被重啟,可以為該參數添加 noinline 修飾符。
@Composable
inline fun RestartableColumn(noinline content: @Composable ColumnScope.() -> Unit) {
Column {
content()
}
}跳過 Compose 方法執行
雖然 Compose 會盡量限制重組范圍,但仍可能執行一些無需更新的 Compose 方法。為避免這種非必要的執行,Compose 編譯器會為 Compose 方法插入跳過邏輯,從而在無需更新時自動跳過方法體執行。
哪些 Compose 方法不可跳過
未開啟 Strong skipping mode 時編譯器還會判斷方法參數的穩定性,以此決定是否為該方法生成跳過邏輯,但在 kotlin 2.0.20 后該功能默認開啟,所以本文的原理分析均在該功能開啟的前提下進行。
正如不是所有 Compose 方法都可以被重啟一樣,也不是所有 Compose 方法都可以被跳過。
我們可以通過閱讀 Compose 編譯器源碼來了解哪些方法無法被跳過,具體邏輯在 ComposableFunctionBodyTransformer#visitFunctionInScope 中,代碼如下:



總結下來就是「不可被重啟的方法同樣不可被跳過」,編譯器不會為不可重啟的方法生成 Skip 相關邏輯。
此外 Compose 還提供了 @NonSkippableComposable 注解,允許開發者手動指定某個 Compose 方法不可跳過。
如何跳過執行
$changed 參數揭秘
編譯器首先會為 Compose 方法插入一個參數 $changed,用于表示當前方法各個參數的變化狀態,為后續判斷是否能夠跳過重組提供輔助信息。
$changed 是 Int 類型,每三位保存一個參數的信息,最低位用來表示是否強制重組,因此一個 $changed 能夠保存 10 個參數的信息。如果參數個數大于 10,那么就會添加 $changed1、$changed2,以此類推。整體結構如下:

每個參數使用了 3 位來保存信息,其中低兩位用來表示參數是否變化,最高位表示當前參數是否穩定(Stable)。

參數變化信息有以下 4 種取值。

- Uncertain (0b000):無法確定該參數較上一次重組是否有變化
- Same (0b001):該參數較上一次重組沒有發生變化
- Different (0b010):該參數較上一次重組發生了變化
- Static (0b011):該參數為靜態對象,在 Compose 的生命周期內不會發生變化
生成 $dirty 跳過執行
Compose 方法是否跳過的判斷條件為「所有被使用的參數相比上一次重組均沒有發生變化」,所以 Compose 編譯器會結合 $changed 參數依次確認每個參數是否變化,并最終決定是否跳過執行。以一個簡單的例子來分析編譯生成的跳過邏輯。
@Composable
fun ComposeDemo(param1: Int, param2: Int) {
println("$param1 $param2")
}
// 編譯后代碼
@Composable
fun ComposeDemo(param1: Int, param2: Int, $composer: Composer?, $changed: Int) {
val $dirty = $changed
// 判斷第一個參數是否是 Uncertain 0b000
if ($changed and 0b0110 == 0) {
// 通過 $composer.changed 來判斷參數是否發生變化,并更新 $dirty
$dirty = $dirty or if ($composer.changed(param1)) 0b0100 else 0b0010
}
// 同樣的方式判斷第二個參數
if ($changed and 0b00110000 == 0) {
$dirty = $dirty or if ($composer.changed(param2)) 0b00100000 else 0b00010000
}
// 判斷是否跳過
if ($dirty and 0b00010011 != 0b00010010 || !$composer.skipping) {
println("$param1 $param2")
} else {
$composer.skipToGroupEnd()
}
}$composer.changed 是 Compose 運行時提供用于判斷參數是否變化的方法,不在本文討論范圍內。
首先生成變量 $dirty 并賦值為 $changed,用于表示每個參數最終的變化狀態。
隨后會對每個參數進行判斷,當某個參數變化信息為 Uncertain 時,會通過 Composer 來判斷參數是否發生變化,并更新$dirty。以第一個參數為例,當$composer.changed返回 true 時會執行 $dirty or 0b0100,也就是將 $dirty 中表示第一個參數狀態的第二位置為 1,從 Uncertain 變為 Different,反之則是置為 Same。
完成所有參數校驗后會判斷 $dirty and 0b00010011 != 0b00010010,如果為 true 則執行方法,也就是說想要跳過執行需要滿足 $dirty and 0b00010011 == 0b00010010。該判斷的含義為:
- 最低位需要為 0,表示當前并非強制重組,否則就需要執行方法
- 兩個參數的最低位都需要為 1,也就是兩個參數都是 Same 或 Static,邏輯上就是參數較上一次重組沒有發生變化
關于穩定性請參考官方文檔
https://developer.android.com/develop/ui/compose/performance/stability
前面提到每個參數的最高位表示穩定性,對于不穩定的參數 Compose 會采用不同的方法來判斷是否變化,由于上面的例子中參數均為編譯期可推斷的穩定類型(Int),所以采用了 $composer.changed 來判斷。
如果我們將第二個參數類型改為編譯期無法推斷的類型,那么生成的邏輯將會有所變化。
interface InterfaceType
@Composable
fun ComposeDemo(param: InterfaceType) {
println("$param1 $param2")
}
// 編譯后代碼
@Composable
fun ComposeDemo(param1: Int, param2: InterfaceType, $composer: Composer?, $changed: Int) {
val $dirty = $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($composer.changed(param1)) 0b0100 else 0b0010
}
if ($changed and 0b00110000 == 0) {
$dirty = $dirty or if (if ($changed and 0b01000000 == 0) { // 判斷參數穩定性
$composer.changed(param2)
} else {
$composer.changedInstance(param2)
}
) 0b00100000 else 0b00010000
}
if ($dirty and 0b00010011 != 0b00010010 || !$composer.skipping) {
println("$param1 $param2")
} else {
$composer.skipToGroupEnd()
}
}可以看到針對第二個參數首先會判斷最高位。
- 如果是0則為穩定類型,通過 $composer.changed 判斷,本質上是通過==來比較重組前后的參數
- 如果是1則為不穩定類型,通過 $composer.changedInstance 判斷,本質上是通過===來比較重組前后的參數
而對于未使用的參數,Compose 編譯器也會非常智能的忽略它,減少不必要的運算開銷。去掉 ComposeDemo 中對 param2 的使用后,反編譯代碼如下所示,可以看到只判斷了 param1 的變化情況。
@Composable
fun ComposeDemo(param1: Int, param2: Int) {
println("$param1")
}
// 編譯后代碼
@Composable
fun ComposeDemo(param1: Int, param2: Int, $composer: Composer?, $changed: Int) {
val $dirty = $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($composer.changed(param1)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
println("$param1")
} else {
$composer.skipToGroupEnd()
}
}$changed 信息傳遞
如果沒有$changed 參數,Compose 仍可以通過 $composer.changed 來判斷參數是否發生變化,也能夠正常實現跳過邏輯。但是 $composer.changed 是比較重的操作,如果 $changed 參數已經提供了足夠的信息,那么就可以避免調用 $composer.changed,極大提升運行時性能,這也是 $changed 的設計初衷。
下面我們來看一下 $changed 參數在各個場景下如何為 Compose 提供有效信息。
靜態參數信息
當調用方遞靜態對象(同一個對象或值相同的基礎類型)作為參數時,編譯器會將 $changed 對應參數信息設置為 Static(011),這樣被調用的 Composable 方法就可以直接跳過這個參數的對比。
在下面的例子中, 編譯器識別出 ComposeScreen 傳入的參數為常量 1,所以傳遞 $changed 值 0b0110 將參數設置為 Static。
@Composable
fun MyComposeUI(param: Int) {
println("$param")
}
@Composable
fun ComposeScreen(param: UnstableImpl) {
MyComposeUI(1)
}
// 編譯后代碼
@Composable
fun ComposeScreen(param: UnstableImpl, $composer: Composer?, $changed: Int) {
if ($changed and 0b0001 != 0 || !$composer.skipping) {
MyComposeUI(1, $composer, 0b0110)
} else {
$composer.skipToGroupEnd()
}
}除了直接傳遞常量的場景外,我們也可以通過在方法或屬性上標注 @Stable 來幫助編譯器識別方法或屬性的值是否是靜態對象,這種場景下 @Stable 的作用是告訴編譯器:
- 方法:該方法的輸入不變時,方法返回值也保持不變
- 屬性:任意時刻該屬性的返回值保持不變
修改上面的例子,將參數改為調用 stableFunction,生成代碼如下:
@Stable
fun stableFunction(value: Int): Int {
return value + 1
}
@Composable
fun ComposeScreen(param: UnstableImpl) {
MyComposeUI(stableFunction(1))
}
// 編譯后代碼
@Composable
fun ComposeScreen(param: UnstableImpl, $composer: Composer?, $changed: Int) {
if ($changed and 0b0001 != 0 || !$composer.skipping) {
MyComposeUI(stableFunction(1), $composer, 0b0110)
} else {
$composer.skipToGroupEnd()
}
}盡管是將方法的返回值作為參數傳遞,但編譯器仍然能夠識別到該參數為靜態參數,就是因為 stableFunction 被標記為@Stable,且ComposeScreen調用 stableFunction 傳遞的是一個常量。
這種方法在 Compose 內部也有普遍的使用,比如經常作為參數使用的 Alignment。

同時 Compose 編譯器也將一些常用的 Kotlin 標準庫方法視為 Stable,比如 listOf(1, 2, 3) 這樣的調用就會被認為返回值是一個靜態對象,這些內置的 Stable 方法在源碼中可以找到。

Compose 編譯器對靜態參數的識別還遠不止于此,下表列出了 Compose 編譯器能夠識別的大部分場景。
場景 | 代碼塊 |
基礎類型常量 | 4 |
基礎類型常量運算 | (1f + 3f) / 2 |
字符串常量 | "Hello world!" |
Object | object Singleton |
Stable function+常量 | /* @Stable fun stableFunction(x: Int) = x.toString() */ stableFunction(42) |
listOf+常量 | listOf('a', 'b', 'c') |
emptyList | emptyList<Any?>() |
mapOf+常量 | mapOf("a" to 42) |
emptyMap | emptyMap<Any, Any?>() |
Pair+常量 | 'a' to 42 |
枚舉 | /* enum class Foo { Bar, Bam } */ Foo.Bar |
Dp+常量 | Dp(4f) |
Dp 常量運算 | 2 * 4.dp |
@Immutable/@Stable+所有屬性都是 Static | KeyboardOptions(autoCorrect = false) PaddingValues(all = 16.dp) |
參數變化信息
在某些場景下調用方會直接將自己的參數傳遞給下一個 Composable 方法,由于該參數在調用方內部已經做過一次判斷,因此可以直接將判斷的結果通過$changed 傳遞下去,省去后面對該參數的判斷成本。
在下面的例子中,ComposeScreen 將自身的參數 param 透傳給 MyComposeUI,編譯器生成的代碼中直接通過 $dirty & 0b1110 獲取到 param 的變化信息并傳遞給 MyComposeUI。
@Composable
fun ComposeScreen(param: Int) {
MyComposeUI(param)
}
// 編譯后代碼
@Composable
fun ComposeScreen(param: Int, $composer: Composer?, $changed: Int) {
val $dirty = $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($composer.changed(param)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
MyComposeUI(param, $composer, 0b1110 and $dirty)
} else {
$composer.skipToGroupEnd()
}
}處理默認參數
Kotlin 支持方法參數的默認值,原理上會在編譯期為方法添加一個 $default 參數用于判斷某個參數是否使用默認值,并在方法開頭為使用了默認值的參數賦值。
而針對 Composable 函數中的參數默認值,Compose 選擇了自己處理而不是交給 Kotlin 編譯器,因為需要處理默認值對跳過邏輯的影響,以一個簡單的例子看一下生成的代碼。
@Composable
fun DefaultTest(param: Int = 1) {
println(param)
}
// 編譯后代碼
@Composable
fun DefaultTest(param: Int, $composer: Composer?, $changed: Int, $default: Int) {
val $dirty = $changed
if ($default and 0b0001 != 0) {
// 使用默認值則設置為 Static
$dirty = $dirty or 0b0110
} elseif ($changed and 0b0110 == 0) {
// 未使用默認值正常判斷 changed
$dirty = $dirty or if ($composer.changed(param)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
if ($default and 0b0001 != 0) {
// 使用默認值時為參數賦值
param = 1
}
println(param)
} else {
$composer.skipToGroupEnd()
}
}和 Kotlin 默認參數處理思路是一樣的,處理流程為:
- 為方法增加 $default 參數,每一位表示對應參數是否使用默認值
- 如果參數使用了默認值,則設置 $dirty 設置為 Static,跳過判斷
- 如果參數未使用默認值,則正常走 changed 判斷
- 如果最終無法跳過當前 Composable 執行,則為使用了默認值的參數賦值
看到這個代碼不由得會產生一個疑問:為什么 param 一旦使用默認值,就可以被判定為 Static ?如果上一次組合調用 DefaultTest 沒用默認值,而這次重組用了默認值,那么 param 不就發生變化了嗎?
其實仔細想想就可以理解:如果某次重組時 param 使用了默認值,那么在整個 Composition 周期內它必然始終都會使用默認值。這是由調用點在編譯期就決定的,一旦出現非默認值的情況,就意味著調用點發生了變化,兩次調用本質上已不再屬于同一個 Compose UI。
不過在這個例子中默認值是 1,前面介紹過對于這種常量 Compose 能夠識別為 Static 對象,如果我們將默認值改為一個方法調用會發生什么?
@Composable
fun DefaultTest1(param: Int = getInt()) {
println(param)
}
fun getInt(): Int {
return 1
}
// 編譯后代碼
@Composable
fun DefaultTest1(param: Int, $composer: Composer?, $changed: Int, $default: Int) {
val $dirty = $changed
// 首先判斷 $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($default and 0b0001 == 0 && $composer.changed(param)) 0b0100 else 0b0010
}
if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
$composer.startDefaults()
if ($changed and 0b0001 == 0 || $composer.defaultsInvalid) {
if ($default and 0b0001 != 0) {
param = getInt()
// 將$dirty 中參數對應信息設置為 000 -> Uncertain
$dirty = $dirty and 0b1110.inv()
}
} else {
$composer.skipToGroupEnd()
if ($default and 0b0001 != 0) {
$dirty = $dirty and 0b1110.inv()
}
}
$composer.endDefaults()
println(param)
} else {
$composer.skipToGroupEnd()
}
}將 param 默認值改為 getInt 調用后,由于 Compose 無法推測該調用是否是 Static ,所以 $dirty 生成的策略有所變化
- 優先檢查 $changed
- 如果已有參數變化信息,直接使用,無需額外判斷。
- 若無變化信息,則判斷默認值使用情況
- 使用了默認值,則設置為 Same。
- 未使用默認值,按常規通過 changed 判斷。
此外,在后續無法跳過執行需要為參數賦值時,Compose 還會增加一段邏輯:將 $dirty 中參數對應信息設置為 Uncertaion(0b000)。
這么做的原因是:雖然使用默認值時被標記為了 Same,但由于這類調用不是 Static,Compose 實際上無法保證其是否真的沒有發生變化。為了避免對子 Composable 的判斷產生誤導,最終將其標記為 Uncertain,從而強制子 Composable 重新進行判斷。在源碼中也可以看到官方的解釋。

如何處理 Composable Lambda
上面討論的場景以及例子都是針對普通 @Composable 方法,而對于 Composable Lambda 的處理稍有不同。Compose 編譯器會將 Composable Lambda 分為三類,并采用不同的處理策略。
無法跳過執行的Composable Lambda
需要注意的是,@NonRestartableComposable、@NonSkippableComposable 對 Lambda 無效。
這部分前面已經介紹過,如果 Composable Lambda 有返回值或者是作為 inline 方法的參數,那么該 Composable Lambda 則無法跳過執行,編譯器不會做任何的優化。
@Composable
fun TestComposeLambda() {
// 有返回值的 Composable Lambda
val lambda = @Composable { text: String ->
println("ComposeLambda: $text")
""
}
}
// 編譯后代碼
fun TestComposeLambda($composer: Composer?, $changed: Int) {
if ($changed != 0 || !$composer.skipping) {
val lambda = { text: String, $composer: Composer?, $changed: Int ->
$composer.startReplaceGroup(1957901905)
println("ComposeLambda: $text")
$composer.endReplaceGroup()
tmp0
}
} else {
$composer.skipToGroupEnd()
}
}可跳過執行的Composable Lambda
對于可正常跳過執行的 Composable Lambda,編譯器會對其進行一層封裝,具體封裝邏輯取決于該 Lambda 是否捕獲外部變量。
不捕獲外部變量
在 Kotlin 中,一個不捕獲外部變量的 Lambda 最終會被優化為一個單例,因為這種 Lambda 沒有任何狀態,優化為單例對邏輯沒有任何影響且能夠節省運行開銷。
類似的,針對不捕獲外部變量的 Composable Lambda,Compose 編譯器也會為期生成一個單例,同時通過 composableLambdaInstance 進行封裝。
@Composable
fun TestComposeLambda() {
// 無狀態 Composable Lambda
val lambda = @Composable { text: String ->
println("ComposeLambda: $text")
}
}
// 編譯后代碼
fun TestComposeLambda($composer: Composer?, $changed: Int) {
if ($changed != 0 || !$composer.skipping) {
val lambda = ComposableSingletons$ComposeLambdaTestKt.lambda$1010909634
} else {
$composer.skipToGroupEnd()
}
}
// 生成單例
internal object ComposableSingletons$ComposeLambdaTestKt {
// 使用 composableLambdaInstance 封裝 Lambda
val lambda$1010909634: Function3<String, Composer, Int, Unit> = composableLambdaInstance(1010909634, false) { text: String, $composer: Composer?, $changed: Int ->
val $dirty = $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($composer.changed(text)) 0b0100 else 0b0010
}
if ($dirty and 0b00010011 != 0b00010010 || !$composer.skipping) {
println("ComposeLambda: $text")
} else {
$composer.skipToGroupEnd()
}
}
}捕獲外部變量
如果 Composable Lambda 捕獲了外部變量,則無法優化為單例。這種情況下 Compose 會使用 remember 來緩存該 Composable Lambda 對象,避免每次重組都會創建新的 Lambda 實例。
@Composable
fun TestComposeLambda() {
var name: String = ""
// 捕獲外部變量 name
val lambda = @Composable { text: String ->
println("ComposeLambda: $text $name")
}
}
// 編譯后代碼
fun TestComposeLambda($composer: Composer?, $changed: Int) {
if ($changed != 0 || !$composer.skipping) {
val lambda = rememberComposableLambda(2141696259, true, { text: String, $composer: Composer?, $changed: Int ->
val $dirty = $changed
if ($changed and 0b0110 == 0) {
$dirty = $dirty or if ($composer.changed(text)) 0b0100 else 0b0010
}
if ($dirty and 0b00010011 != 0b00010010 || !$composer.skipping) {
println("ComposeLambda: $text $name")
} else {
$composer.skipToGroupEnd()
}
}, $composer, 0b00110110)
} else {
$composer.skipToGroupEnd()
}
}rememberComposableLambda實際上是基于 remember 創建 Lambda 對象。























