精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

最新一区二区三区| 天堂社区在线视频| 性高潮久久久久久久久久| 免费在线亚洲欧美| 中文字幕亚洲激情| 美女被艹视频网站| 大桥未久在线视频| 国产欧美精品日韩区二区麻豆天美| 国产精品十八以下禁看| 国产美女久久久久久| 日日天天久久| 欧美美女视频在线观看| 日韩精品在线视频免费观看| 性感美女一级片| 国产一区二区三区日韩| 97视频在线观看免费| 国产aaaaaaaaa| av成人app永久免费| 欧美日韩在线视频一区| 亚洲午夜在线观看| 天天综合天天综合| 国产乱理伦片在线观看夜一区| 57pao国产成人免费| 亚洲波多野结衣| 日韩美女国产精品| 日韩久久精品一区| 视频二区在线播放| 国产免费不卡| 亚洲国产成人av网| 五月天在线免费视频| 国产高清免费av在线| 99久久国产综合精品色伊| 91在线视频九色| 中文字幕第一页在线播放| 99国产精品久久久久久久成人热 | av大片在线观看| www.欧美.com| 国产a一区二区| 国产熟女一区二区丰满| 免费在线成人网| 国产suv精品一区二区| 国产精品999久久久| 亚洲一区色图| 久久久久北条麻妃免费看| 快灬快灬一下爽蜜桃在线观看| 日本三级久久| 日韩成人激情视频| 中国黄色片视频| 亚洲精品在线国产| 欧美一区二区美女| 女王人厕视频2ⅴk| 年轻的保姆91精品| 日韩精品自拍偷拍| 中文在线字幕观看| 伊人精品综合| 精品久久久久久久久久久久包黑料| 精品国产鲁一鲁一区二区三区| 欧美成人一二区| 欧美日韩国产首页| 亚洲无在线观看| 国产亚洲高清一区| 欧美不卡一区二区三区四区| 无码国产精品久久一区免费| 日韩不卡在线视频| 精品久久人人做人人爽| 动漫美女无遮挡免费| 精品国产一区二区三区成人影院 | 亚洲第一成人网站| 国产色噜噜噜91在线精品| 亚洲成成品网站| 免费无码一区二区三区| 免费精品国产| 中文字幕亚洲字幕| 国产精品久久久精品四季影院| 亚洲欧美色图| 国精产品一区一区三区有限在线| 日韩免费黄色片| 日欧美一区二区| 国产美女久久久| av在线免费在线观看| 高清不卡一区二区| 久久综合入口| 午夜视频在线观看网站| 亚洲欧美aⅴ...| 免费看又黄又无码的网站| 黑人巨大精品欧美一区二区桃花岛| 在线视频你懂得一区| wwwwwxxxx日本| 97se亚洲| 国产亚洲激情视频在线| 中文字幕av播放| 中日韩视频在线观看| 国产精品三级美女白浆呻吟| 国产精品国精产品一二| 丝袜a∨在线一区二区三区不卡| 日本在线精品视频| 一级片免费网站| 成人亚洲一区二区一| 欧美综合激情| 三级资源在线| 欧美怡红院视频| 国产乱淫av片| 成人羞羞视频在线看网址| 欧美激情a∨在线视频播放| 精品不卡一区二区| 国产精品综合av一区二区国产馆| 国产综合 伊人色| 91视频在线观看| 亚洲成a天堂v人片| 污网站在线免费| 一区二区三区视频免费观看| 欧美xxxx18国产| 精品成人无码久久久久久| 国产精品18久久久| 亚洲国产午夜伦理片大全在线观看网站 | 天天躁日日躁aaaxxⅹ| 亚洲男女av一区二区| 国产精欧美一区二区三区| 性一交一乱一乱一视频| 国产精品欧美经典| 日本免费一级视频| 国产精品115| 久久99精品久久久久久琪琪| 中文永久免费观看| 久久先锋影音av| 91成人在线观看喷潮教学| 国产精品毛片无码| 色偷偷亚洲男人天堂| 免费看一级视频| 91在线丨porny丨国产| 国产日韩欧美大片| 欧美成人福利| 日韩在线免费高清视频| 波多野结衣日韩| 久久综合九色综合97婷婷| 国产精品久久..4399| 欧美日韩午夜电影网| 深夜福利亚洲导航| 日本妇乱大交xxxxx| 久久只精品国产| av动漫在线观看| 婷婷激情久久| 91av在线播放视频| 日批视频在线播放| 亚洲成a天堂v人片| 国产熟女高潮一区二区三区| 妖精视频成人观看www| 国产福利一区二区三区在线观看| 91最新在线视频| 日韩一区二区视频| 欧美日韩三级在线观看| 国产乱码精品一区二区三区忘忧草| 在线观看成人一级片| 国产成人精品一区二区三区视频| 亚洲最新av在线| 中文字幕永久免费视频| 欧美—级在线免费片| 波多野结衣天堂| 日韩欧美一区二区三区在线视频| 国产精品黄视频| av天在线观看| 欧美日韩一区二区欧美激情| 三级黄色录像视频| 国产精品一区二区在线观看不卡| 免费看污污视频| 超碰成人在线免费| 欧美一级bbbbb性bbbb喷潮片| 性感美女视频一二三| 欧美在线不卡一区| 91香蕉视频在线播放| 国产成人精品免费在线| 男女激情无遮挡| 国产成人1区| 91久久精品国产| 国产网红在线观看| 亚洲欧美中文日韩在线| 中文字幕一区二区三区波野结| 亚洲视频香蕉人妖| 国产xxxxxxxxx| 日韩有码一区二区三区| 老司机av福利| 猫咪成人在线观看| 国产精品日韩专区| 欧美aaaaaaa| 亚洲少妇激情视频| 国产日韩欧美中文字幕| 精品日韩中文字幕| 91大神福利视频| 成人h动漫精品| 少妇黄色一级片| 国产精品sm| 欧美久久在线| 试看120秒一区二区三区| 97av在线视频| 黄色免费在线网站| 精品视频在线播放色网色视频| 中文字幕人妻色偷偷久久| 亚洲影院在线观看| av永久免费观看| 成人免费毛片a| 在线观看亚洲色图| 一区二区毛片| 性做爰过程免费播放| 天海翼亚洲一区二区三区| 91久久久在线| 日日夜夜天天综合| 欧美精品999| 欧美精品日韩少妇| 亚洲欧美成人在线| www.久久伊人| 欧美亚洲图片小说| 日韩在线视频免费播放| 亚洲欧美偷拍另类a∨色屁股| 国产又爽又黄无码无遮挡在线观看| 久久91精品国产91久久小草| 欧美色图另类小说| 欧美人成网站| 亚洲精品高清视频| 亚洲精品中文字幕99999| 成人在线观看av| 青青在线精品| 国产精品96久久久久久| 日本午夜大片a在线观看| 久久成人精品一区二区三区| √天堂资源地址在线官网| 亚洲精品少妇网址| 日本国产在线观看| 欧美岛国在线观看| 国产又大又长又粗| 欧美偷拍一区二区| 无码人妻精品一区二区蜜桃色欲 | 欧美亚洲色图视频| 先锋资源久久| 伊人狠狠色丁香综合尤物| 精品久久网站| 久久综合狠狠综合久久综青草| 成人知道污网站| 5566av亚洲| 国产精品igao视频网网址不卡日韩 | 亚洲国产精品视频一区| 国产一区二区三区91| 蜜桃传媒一区二区| 日韩母乳在线| 欧美黑人3p| 宅男在线一区| 日本日本精品二区免费| 国产videos久久| 午夜精品视频在线观看一区二区| 国产不卡av一区二区| 日韩国产精品一区二区三区| 久久97视频| 日韩欧美一区二区视频在线播放| 国产精品亚洲片在线播放| 日本一区二区免费看| 精品一区二区三| 亚洲国产精品视频一区| 亚洲91精品| 久久久无码中文字幕久...| 国内精品久久久久久久影视麻豆| 欧美大片免费播放| 亚洲国产一区二区三区a毛片| 日本a在线免费观看| 亚洲深夜影院| 欧美伦理片在线看| 麻豆精品久久久| 亚洲精品在线网址| 成人久久视频在线观看| 波多野结衣影院| 国产日产欧美一区| 99久久精品久久亚洲精品| 亚洲乱码一区二区三区在线观看| 久草视频中文在线| 午夜电影一区二区三区| 99成人精品视频| 欧美一区二区三级| 涩爱av在线播放一区二区| 国产亚洲精品美女久久久| 欧美r级在线| 国产69精品久久久| 高清电影一区| 亚洲直播在线一区| 米奇精品关键词| 亚洲精品一区二区三区四区五区| 一区二区三区网站| 日本精品免费在线观看| 久久99久久久久| 小毛片在线观看| 国产精品色一区二区三区| 青青草偷拍视频| 色94色欧美sute亚洲线路一久| 国产又大又粗又长| 精品中文字幕久久久久久| 一广人看www在线观看免费视频| 欧美高清在线播放| 97久久香蕉国产线看观看| 91国产在线免费观看| 免费看av成人| 日本香蕉视频在线观看| 日韩国产一区二| 亚洲一区二区三区四区av| 国产精品免费看片| 99精品视频99| 91精品国产麻豆国产自产在线 | av综合在线播放| 中文字幕求饶的少妇| 黑人巨大精品欧美一区二区一视频| 91亚洲欧美激情| 亚洲人av在线影院| 久久久久黄久久免费漫画| 国产一区玩具在线观看| 日韩三级视频| 毛片av在线播放| 久久99精品国产.久久久久| 短视频在线观看| 亚洲国产乱码最新视频| 国产精品视频一区二区三区,| 亚洲男人天堂手机在线| 国产羞羞视频在线播放| 亚洲自拍偷拍第一页| 日韩欧美高清在线播放| 欧美一级黄色影院| 97精品久久久午夜一区二区三区| 色欲人妻综合网| 欧美理论电影在线| 国产黄色片在线观看| 热99在线视频| 久久精品色综合| 成人免费性视频| 国产精品一级在线| 97成人资源站| 欧美日韩视频专区在线播放| 国产一区二区三区不卡在线| 91av在线网站| 美女视频亚洲色图| 成人免费播放器| 成人黄色网址在线观看| 激情视频在线播放| 欧美一区二区三区四区视频| 麻豆传媒视频在线观看免费| 国产欧美精品va在线观看| 欧美久久综合网| 美女喷白浆视频| 欧美激情一区二区在线| 自拍偷拍精品视频| 中文精品99久久国产香蕉| 成人涩涩视频| 亚洲欧洲日韩精品| 久草在线在线精品观看| 国产极品美女在线| 91精品福利在线一区二区三区| 国产视频一区二区| 成人乱色短篇合集| 午夜精品av| 久久久久久久人妻无码中文字幕爆| 亚洲国产精品欧美一二99| 色偷偷在线观看| 欧美在线亚洲一区| 精品国产视频| 国产美女视频免费看| 亚洲乱码国产乱码精品精可以看| 性一交一乱一透一a级| 性欧美xxxx交| 欧美精美视频| 日本中文字幕精品—区二区| 亚洲人成在线观看一区二区| 国产ts变态重口人妖hd| 国产+人+亚洲| 狠狠操综合网| 天美一区二区三区| 亚洲国产一区在线观看| 欧洲视频在线免费观看| 国产精品久久久久久久app| 亚洲澳门在线| 午夜免费福利影院| 色屁屁一区二区| 成人video亚洲精品| 国产免费一区| 日韩国产高清影视| 成人免费视频网站入口::| 亚洲国产精品资源| 外国电影一区二区| 免费的一级黄色片| 2023国产精品视频| 国产精品久久久久久久久毛片 | 国产亚洲成av人片在线观看桃| 国产毛片精品久久| 激情小视频网站| 亚洲国产成人一区二区三区| 亚洲av无码片一区二区三区 | 久久婷婷色综合| 亚洲在线观看av| 午夜免费日韩视频| 久久一区二区中文字幕| 国产黑丝在线观看| 欧美视频在线一区二区三区 | 精品伦精品一区二区三区视频| 日韩va亚洲va欧美va久久| 精品在线视频观看| 日韩专区在线观看| 九九久久成人|