精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

開發 移動開發
Android View 通過測量、布局和繪制三個階段完成 UI 渲染,Compose 整體上與 Android View 類似,但在開頭多了一個叫做“組合”的重要階段。在組合階段,Compose 會執行 @Composable 方法并輸出 UI 的樹狀結構與對應信息,為后續的布局階段提供數據基礎。

什么是智能重組

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 執行邏輯如下:

  1. RecomposeDemo 執行
  2. RecomposeDemo 啟動 RestartGroup
  3. 讀取 uiState
  4. 調用 ComposeUI
    a. ComposeUI 啟動 RestartGroup
    b. ComposeUI 結束 RestartGroup
  5. 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 生成的策略有所變化

  1. 優先檢查 $changed
  • 如果已有參數變化信息,直接使用,無需額外判斷。
  1. 若無變化信息,則判斷默認值使用情況
  • 使用了默認值,則設置為 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 對象。



責任編輯:龐桂玉 來源: 字節跳動技術團隊
相關推薦

2023-03-26 20:39:01

2010-03-23 11:17:16

Python 動態編譯

2010-10-20 13:43:37

C++編譯器

2022-05-18 09:31:42

編譯器開源代碼生成

2010-01-18 10:34:21

C++編譯器

2010-01-21 09:11:38

C++編譯器

2010-01-12 16:42:59

C++編譯器

2009-08-10 17:12:54

C#編譯器

2013-03-29 10:02:37

編譯器語言編譯開發

2017-03-20 18:01:55

編譯器匯編

2009-07-07 09:14:53

Milepost GC編譯器

2017-07-24 13:13:00

智能AICIO

2017-04-07 11:12:22

智能黑科技汽車

2010-01-14 16:46:13

CentOS Mysq

2019-08-06 08:20:07

編譯器工具開發者

2013-12-30 11:21:31

Go編譯器

2022-11-24 13:05:27

ClangiOS

2011-05-18 11:06:25

java編譯器

2010-03-02 10:55:47

Linux SkyEy

2009-08-06 14:59:36

C#編譯器
點贊
收藏

51CTO技術棧公眾號

免费毛片一区二区三区| 精品影片一区二区入口| 主播国产精品| 91最新地址在线播放| 日av在线播放中文不卡| 中国1级黄色片| 日韩一区二区三区在线看| 午夜精品一区二区三区免费视频| 久久综合一区| 国产精品国产一区二区三区四区 | 日韩精品免费视频| 在线观看的毛片| 丁香高清在线观看完整电影视频| 久久精品视频免费| 成人精品在线观看| 精品在线播放视频| 色婷婷色综合| 日韩精品在线第一页| 亚洲18在线看污www麻豆| 白白色在线观看| 国产精品久久一级| 欧美人xxxxx| av免费在线不卡| 日韩av一二三| 18久久久久久| 麻豆亚洲av熟女国产一区二| 欧美日韩精品一区二区视频| 精品第一国产综合精品aⅴ| 久久这里只精品| 狠狠操一区二区三区| 亚洲视频免费在线观看| 日韩精品极品视频在线观看免费| 亚洲狼人综合网| 另类成人小视频在线| 欧美又大又粗又长| 久久精品无码人妻| 国产精品久久久久久影院8一贰佰 国产精品久久久久久麻豆一区软件 | 茄子视频成人在线观看 | 日韩三级一区二区三区| 久久精品青草| 色777狠狠综合秋免鲁丝| 少妇大叫太粗太大爽一区二区| 视频一区视频二区欧美| 69堂精品视频| 精品亚洲一区二区三区四区| 欧美电影免费观看网站| 精品久久久一区| 欧美a级免费视频| caoporn免费在线| 中文字幕一区二区三中文字幕| 久久综合久久久| 亚洲色图欧美视频| 成人精品视频.| 翡翠波斯猫1977年美国| 亚洲av无码国产精品久久不卡| 韩国三级在线一区| 91老司机精品视频| 一区二区 亚洲| 精品一区精品二区高清| 国产日产亚洲精品| 一区二区三区在线免费观看视频| 日韩和欧美的一区| 国产精品成人av在线| 手机av免费观看| 日韩激情视频在线观看| 国产精品黄色av| 中文字幕在线观看精品| 看片的网站亚洲| 成人免费福利在线| 99在线小视频| 不卡的av中国片| 精品毛片久久久久久| 天天躁日日躁狠狠躁伊人| 99久久99久久久精品齐齐| 精品综合在线| 国产粉嫩一区二区三区在线观看| 国产日韩欧美综合一区| 亚洲精品视频一二三| 免费av在线| 亚洲一区二区三区视频在线 | 国产精品一品二品| 国产成人精品日本亚洲11| 国精产品一品二品国精品69xx| 成人精品鲁一区一区二区| 久久福利电影| 成在在线免费视频| 亚洲色图在线看| 99色这里只有精品| 桃花岛成人影院| 337p亚洲精品色噜噜噜| 国产精品扒开腿做爽爽爽a片唱戏| 日韩啪啪网站| 色琪琪综合男人的天堂aⅴ视频| 91杏吧porn蝌蚪| 一区二区三区高清视频在线观看| 国产激情综合五月久久| 中文字幕丰满人伦在线| 老汉av免费一区二区三区| 97超碰最新| 亚洲人成色777777精品音频| 久久精品视频免费观看| 国产在线精品一区二区中文| 国产在线91| 亚洲精品免费在线播放| 国产精品无码一区二区在线| 精品免费av一区二区三区| 欧美酷刑日本凌虐凌虐| 日本少妇一级片| 亚洲专区视频| 色综合久久久888| 亚洲免费在线观看av| 青青草国产精品97视觉盛宴| 51精品国产人成在线观看| 污污网站在线免费观看| 国产精品人人做人人爽人人添| 亚洲国产精品女人| 免费一二一二在线视频| 欧美精品tushy高清| 久久久老熟女一区二区三区91| 国语产色综合| 欧美精品videos另类日本| 日日噜噜噜噜人人爽亚洲精品| 精品一区二区免费| 国产一区二区无遮挡| 国产三级在线免费| 一区二区三区高清| 国产视频手机在线播放| 国产成人精品亚洲线观看| 这里只有精品视频| 日韩欧美视频在线免费观看| 黄页网站大全一区二区| 日韩欧美激情一区二区| 国产va在线视频| 在线成人av网站| 全黄一级裸体片| 欧美日韩影院| 国产精品一区二区久久国产| 少妇一区二区三区四区| 一区二区三区四区激情| 无限资源日本好片| 网友自拍区视频精品| 欧美成人免费网| 一级α片免费看刺激高潮视频| av电影天堂一区二区在线| 午夜探花在线观看| 国产精成人品2018| 亚洲精品理论电影| 丝袜美腿小色网| 久久精品国产99久久6| 欧美一区二区三区成人久久片 | 国产白丝袜美女久久久久| 欧美激情三级| 久久精品国亚洲| 中文字幕视频二区| 国产亚洲一区二区三区在线观看| 亚洲 自拍 另类小说综合图区| 欧美日本三级| 日韩一区二区久久久| 91尤物国产福利在线观看| 久久精品在这里| 男人透女人免费视频| 巨人精品**| 91禁外国网站| 天天操天天射天天| 亚洲一区免费视频| 超碰97在线资源站| 中文一区二区| 精品日产一区2区三区黄免费 | 国产精品美女久久久久aⅴ | 石原莉奈在线亚洲三区| 欧美理论一区二区| 免费观看成人性生生活片| 亚洲欧美国产一区二区三区 | 午夜成人免费视频| 91视频在线免费| 黑丝一区二区三区| 国产欧美韩日| 欧美a级在线观看| 亚洲精品网站在线播放gif| av资源免费观看| 久久蜜桃一区二区| 一本色道久久亚洲综合精品蜜桃 | 麻豆精品新av中文字幕| 色噜噜狠狠一区二区三区| 色成人免费网站| 精品亚洲男同gayvideo网站| 中文字幕福利视频| 亚洲免费观看高清| 欧美夫妇交换xxx| 亚洲永久免费| 亚洲高清视频一区| 北岛玲精品视频在线观看| 欧美日本亚洲视频| 少妇喷水在线观看| 在线观看不卡视频| 午夜爽爽爽男女免费观看| 国产jizzjizz一区二区| 国产黄视频在线| 日韩av专区| 99电影网电视剧在线观看| 538在线观看| 亚洲视频777| 99热这里只有精品在线| 精品国产精品三级精品av网址| 男女做爰猛烈刺激| 狠狠网亚洲精品| 成人在线免费观看av| 91影院成人| 精品无人乱码一区二区三区的优势| 欧美成人免费电影| 日韩一区二区久久久| 神宫寺奈绪一区二区三区| 在线观看av一区| 国产亚洲精品成人| 中文字幕一区在线| 国产乱了高清露脸对白| 国内成人自拍视频| 久久精品免费一区二区| 99精品视频在线观看播放| 国产精品污www一区二区三区| 日韩一区二区三区免费| 久久99国产精品久久久久久久久| 你懂的在线观看视频网站| 7777精品伊人久久久大香线蕉| 黄色大片网站在线观看| 亚洲精品一二三| 亚洲а∨天堂久久精品2021| 成人夜色视频网站在线观看| 2025韩国理伦片在线观看| 国产亚洲福利| 800av在线免费观看| 在线精品视频在线观看高清| 日韩精品一区二区三区外面 | 久久久久天天天天| 国产免费区一区二区三视频免费 | 亚洲精品999| 伊人成人在线观看| 色综合久久中文综合久久牛| 久一视频在线观看| 综合自拍亚洲综合图不卡区| 一级黄色性视频| k8久久久一区二区三区| 不卡的av中文字幕| 免费在线观看不卡| 国产免费视频传媒| 亚洲欧美日韩国产综合精品二区| 欧美日韩中文字幕在线播放| 99久久婷婷| 亚洲欧美日韩精品久久久| 国产九一精品| 欧美激情第一页在线观看| 久久97精品| 国产精品乱子乱xxxx| 四虎国产精品免费久久| 国产日韩欧美视频在线| 成人精品国产| 国产精品美女主播| 欧美影视资讯| 国产成人97精品免费看片| 性欧美18xxxhd| 992tv在线成人免费观看| av中文字幕在线看| 欧美日本高清视频| 免费污视频在线观看| 欧美国产在线电影| 97超碰免费在线| 午夜精品一区二区三区视频免费看| 在线观看中文字幕的网站| 九九热精品视频国产| 欧美人与性动交α欧美精品图片| 欧美高清视频在线播放| 欧美xxxx免费虐| 欧美激情性做爰免费视频| 女子免费在线观看视频www| 久久久久久久久久av| 2021天堂中文幕一二区在线观| 97在线观看免费高清| 亚洲精品**中文毛片| 日av在线播放中文不卡| 国产精品4hu.www| 91久久精品国产91久久性色| 精品一区二区三区中文字幕视频| 亚洲综合精品一区二区| 99国产精品免费网站| 国产精品乱码视频| 免费精品国产| 欧美福利一区二区三区| 日韩在线不卡| 欧美大黑帍在线播放| 亚洲国产欧美国产综合一区| 国产美女无遮挡网站| 人人精品人人爱| 一区二区三区四区影院| 99r国产精品| 国产成人免费观看网站| 亚洲男人都懂的| 欧美三日本三级少妇99| 欧美日免费三级在线| www.四虎在线观看| 亚洲精品永久免费| 巨骚激情综合| 欧美激情手机在线视频| 欧美电影h版| 92国产精品视频| 日韩欧美ww| 在线国产精品网| 亚洲三级国产| av亚洲天堂网| caoporn国产一区二区| 三上悠亚ssⅰn939无码播放| 亚洲欧美国产77777| 久久精品视频1| 538prom精品视频线放| 五月天激情婷婷| 亚洲视频欧洲视频| aa级大片免费在线观看| 国产精品久久久久久久久久久久| 亚洲日本va中文字幕| 六月婷婷久久| 亚洲香蕉网站| 久久久久久久久久一区| 不卡av在线免费观看| www.毛片com| 91国偷自产一区二区使用方法| 亚洲成人精品女人久久久| 伊人久久久久久久久久久久久| 国产网红在线观看| 国产精品自产拍在线观看| 精品欠久久久中文字幕加勒比| 黄频视频在线观看| 男女av一区三区二区色多| 在线观看网站黄| 中文字幕成人网| 4438国产精品一区二区| 日韩亚洲国产中文字幕欧美| yw193.com尤物在线| 97热在线精品视频在线观看| 国产麻豆一区二区三区| 中文字幕一区二区三区5566| 久久中文欧美| 亚洲国产欧美视频| 亚洲一区二区精品久久av| 国产又黄又粗又硬| 在线日韩日本国产亚洲| 亚洲三级欧美| 国产视频99| 欧美精品一卡| 波多野吉衣在线视频| 综合久久久久久| 91超薄丝袜肉丝一区二区| 日韩精品福利在线| 成人bbav| 精品无码久久久久国产| 国产一区二区三区的电影| 在线播放av网址| 午夜激情久久久| 天堂在线观看av| 久久久久国产视频| 视频精品一区二区三区| 日韩在线观看a| 成人小视频免费观看| 免费一级片在线观看| 欧美一级淫片007| 色屁屁www国产馆在线观看| 亚洲a∨日韩av高清在线观看| 99久久www免费| 日本高清久久久| 亚洲欧美在线高清| 国产乱淫片视频| 久久九九热免费视频| 成人在线爆射| 精品亚洲第一| 久久www成人_看片免费不卡| av黄色一级片| 欧美视频在线免费| 国产福利在线| 成人性生交大片免费看小说| 98精品久久久久久久| 小明看看成人免费视频| 亚洲一区二区三区四区中文字幕| 日韩一卡二卡在线| 欧美中文字幕视频| 欧美一二区在线观看| 黄色片子免费看| 亚洲丰满少妇videoshd| 亚洲日本在线播放| …久久精品99久久香蕉国产| 久久一区二区三区喷水| 欧美日韩理论片| 亚洲影视在线观看| 免费观看黄一级视频| 欧美亚洲在线视频| 日韩电影免费网站| 超级砰砰砰97免费观看最新一期| 精品女厕一区二区三区| 免费国产在线观看| 91九色视频在线| 日韩午夜精品| 色哟哟一一国产精品| 亚洲第一区中文99精品|