偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

開發(fā) 移動(dòng)開發(fā)
Android View 通過測量、布局和繪制三個(gè)階段完成 UI 渲染,Compose 整體上與 Android View 類似,但在開頭多了一個(gè)叫做“組合”的重要階段。在組合階段,Compose 會(huì)執(zhí)行 @Composable 方法并輸出 UI 的樹狀結(jié)構(gòu)與對應(yīng)信息,為后續(xù)的布局階段提供數(shù)據(jù)基礎(chǔ)。

什么是智能重組

Android View 通過測量、布局和繪制三個(gè)階段完成 UI 渲染,Compose 整體上與 Android View 類似,但在開頭多了一個(gè)叫做“組合”的重要階段。在組合階段,Compose 會(huì)執(zhí)行 @Composable 方法并輸出 UI 的樹狀結(jié)構(gòu)與對應(yīng)信息,為后續(xù)的布局階段提供數(shù)據(jù)基礎(chǔ)。

Compose 采用聲明式 UI 范式,不再像傳統(tǒng) View 那樣通過調(diào)用 View 的 setXXX 方法來手動(dòng)更新 UI,而是在 UI 狀態(tài)變更時(shí)再次執(zhí)行組合、布局、繪制流程,以此完成 UI 的更新,重新組合的過程就叫做“重組“。

然而重組是一個(gè)比較重的過程,需要重新執(zhí)行 @Composable 方法并更新內(nèi)存中關(guān)于 UI 樹的信息,如果每一個(gè)狀態(tài)的變更都要走一遍整個(gè)流程將會(huì)帶來嚴(yán)重的性能問題。因此在 UI 狀態(tài)變化時(shí),Compose 會(huì)智能的選擇必要的 @Composable 方法進(jìn)行重組,并盡可能跳過不必要的代碼執(zhí)行,這就是 Compose 的"智能重組"。

下面的代碼展示了一個(gè)簡單的重組過程,在 Column、Text 組件上設(shè)置了隨機(jī)的背景色,如果它們被重新組合那么背景色就會(huì)隨機(jī)變化,我們可以通過這個(gè)來判斷 UI 是否發(fā)生重組:

@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
    )
}

運(yùn)行效果如下圖所示,點(diǎn)擊第一個(gè) Text 會(huì)觸發(fā) count 變化,從而觸發(fā) UI 的重組。從執(zhí)行結(jié)果來看 Column 和第一個(gè) Text 都發(fā)生了重組,而第二個(gè) Text 并沒有重新執(zhí)行。這也比較符合直覺,畢竟第二個(gè) Text 的內(nèi)容沒有發(fā)生變化,也就不應(yīng)該重組。

然而重組的本質(zhì)就是重新執(zhí)行 @Composable 方法,從代碼邏輯上來說第一個(gè) RecomposeAwareText 被執(zhí)行的情況下,第二個(gè) RecomposeAwareText 也理應(yīng)被執(zhí)行。但正是由于 Compose 的智能重組機(jī)制跳過了不必要的執(zhí)行,從而避免了對第二個(gè) Text 的重組。

智能重組機(jī)制由 Compose 編譯器運(yùn)行時(shí)協(xié)同完成,本文將聚焦于 Compose 編譯器在其中發(fā)揮的作用,徹底揭開智能重組背后的"黑科技"。

編譯器做了什么

為了實(shí)現(xiàn)智能重組能力,Compose 編譯器會(huì)在編譯期對每個(gè) @Composable 方法進(jìn)行轉(zhuǎn)換,插入額外的參數(shù)與控制邏輯。我們將從一個(gè)簡單的示例入手,初步了解編譯器到底做了哪些改動(dòng),建立整體認(rèn)知。后續(xù)章節(jié)將逐步拆解各個(gè)關(guān)鍵環(huán)節(jié),深入解析這些改動(dòng)背后的設(shè)計(jì)原理。

在下面這個(gè)例子中,RecomposeDemo 讀取了 uiState 并將它的值傳遞給 ComposeUI,在 ComposeUI 中將參數(shù) content 進(jìn)行打印。

var uiState by mutableStateOf("UI State")

@Composable
fun RecomposeDemo() {
    ComposeUI(uiState)
}

@Composable
fun ComposeUI(content: String) {
    println(content)
}

經(jīng)過 Compose 編譯器編譯后的代碼如下(僅保留關(guān)鍵的部分):

@Composable
fun RecomposeDemo($composer: Composer?, $changed: Int) {
  $composer = $composer.startRestartGroup(1961523638)
  // 判斷參數(shù)是否變化,如果沒有變化則不執(zhí)行代碼
  if ($changed != 0 || !$composer.skipping) {
    ComposeUI(recordReadValue($readState, "uiState", uiState), $composer, 0)
  } else {
    $composer.skipToGroupEnd()
  }
  $composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
    // 為 RestartGroup 注冊 State 變更時(shí)的回調(diào),重新觸發(fā) RecomposeDemo 執(zhí)行
    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 參數(shù)是否變化
    $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. 插入?yún)?shù)

Compose 編譯器在兩個(gè) @Composable 方法上都增加了 $composer$changed 兩個(gè)參數(shù),$composer 可以看作是當(dāng)前 Compose 的上下文環(huán)境,該參數(shù)會(huì)貫穿整個(gè) Compose 組合階段,在 Composable 方法調(diào)用鏈上層層傳遞。$changed 參數(shù)則是用于提供當(dāng)前方法參數(shù)變化信息,在方法內(nèi)會(huì)結(jié)合該參數(shù)來判斷是否跳過當(dāng)前 @Composable 方法的執(zhí)行。

2. 插入重組邏輯

兩個(gè) @Composable 方法的首尾都插入了 startRestartGroup 和 endRestartGroup 調(diào)用,這其實(shí)是創(chuàng)建了一個(gè) RestartGroup,在這個(gè) Group 內(nèi)如果某個(gè)方法調(diào)用了 State.getValue 方法,那么這個(gè) State 就會(huì)與當(dāng)前的 RestartGroup 綁定,后續(xù)這個(gè) State 變更時(shí)就會(huì)觸發(fā)該 RestartGroup 的執(zhí)行,也就是觸發(fā)重組。

3. 跳過執(zhí)行邏輯

在 ComposeUI 方法中,插入了 $dirty 變量以及對應(yīng)的計(jì)算邏輯,該變量用于最終判斷當(dāng)前方法入?yún)?nbsp;content 是否發(fā)生變化,并根據(jù)該變量來決定是否跳過 ComposeUI 內(nèi)容的執(zhí)行,這是智能重組的核心所在。

創(chuàng)建重組作用域

什么是重組作用域

通過前面對反編譯后代碼的分析,我們知道每個(gè) Compose 方法都被包裝在一個(gè)名為 RestartGroup 的特殊結(jié)構(gòu)中。當(dāng)一個(gè) Compose 方法執(zhí)行時(shí),它會(huì)啟動(dòng)一個(gè) RestartGroup。在這個(gè) RestartGroup 的作用域內(nèi),如果讀取了任何  State,那么這個(gè) State 就會(huì)與當(dāng)前的 RestartGroup 建立關(guān)聯(lián)。當(dāng) Compose 方法執(zhí)行完畢,這個(gè) RestartGroup 也就隨之結(jié)束。

一旦后續(xù)這個(gè) State 的值發(fā)生更新,Compose 就會(huì)自動(dòng)觸發(fā)與該 State 關(guān)聯(lián)的 RestartGroup 進(jìn)行重組。而這個(gè) RestartGroup 所屬的 @Composable 方法,就是我們所說的重組作用域。

@Composable
fun RecomposeDemo($composer: Composer?, $changed: Int) {
  $composer = $composer.startRestartGroup(1961523638)
  // 判斷參數(shù)是否變化,如果沒有變化則不執(zhí)行代碼
  if ($changed != 0 || !$composer.skipping) {
    ComposeUI(recordReadValue($readState, "uiState", uiState), $composer, 0)
  } else {
    $composer.skipToGroupEnd()
  }
  $composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
    // 為 RestartGroup 注冊 State 變更時(shí)的回調(diào),重新觸發(fā) RecomposeDemo 執(zhí)行
    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 參數(shù)是否變化
    $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))
  }
}

還是以第二節(jié)的代碼為例子,RecomposeDemo 執(zhí)行邏輯如下:

  1. RecomposeDemo 執(zhí)行
  2. RecomposeDemo 啟動(dòng) RestartGroup
  3. 讀取 uiState
  4. 調(diào)用 ComposeUI
    a. ComposeUI 啟動(dòng) RestartGroup
    b. ComposeUI 結(jié)束 RestartGroup
  5. RecomposeDemo 結(jié)束 RestartGroup

uiState 被讀取時(shí)處于 RecomposeDemo 的作用域內(nèi),所以后續(xù) uiState 更新時(shí)將會(huì)觸發(fā) RecomposeDemo 的重新執(zhí)行。

哪些 Compose 方法沒有重組作用域

我們稱那些在編譯階段被 Compose 編譯器包裝進(jìn) RestartGroup 的方法是“可重啟”的,但我們需要明確一點(diǎn):并非所有  Compose 方法都能被重啟。這意味著,簡單地認(rèn)為“調(diào)用一個(gè)  Compose 方法就定義了一個(gè)重組作用域”是不準(zhǔn)確的。

重組作用域的范圍會(huì)直接影響性能,而如果我們知道哪些 Composable 不能被重啟,就能寫出更合理的代碼。這樣一旦遇到性能問題,也會(huì)有明確的排查方向。

我們可以通過閱讀 Compose 編譯器源碼來了解哪些方法無法被重啟,具體邏輯在 ComposableFunctionBodyTransformer#shouldBeRestartable 中,代碼如下:

代碼注釋非常詳細(xì),下面介紹其中比較重要的場景。

內(nèi)聯(lián)方法

當(dāng)一個(gè)函數(shù)被內(nèi)聯(lián)后,它就不再擁有一個(gè)獨(dú)立的函數(shù)調(diào)用幀。它的代碼邏輯直接成為了調(diào)用函數(shù)的一部分,所以它就無法作為一個(gè)獨(dú)立的代碼塊進(jìn)行重組。

被 @NonRestartableComposable 標(biāo)記的方法

@NonRestartableComposable 是 Compose 提供的注解,允許開發(fā)者指定某個(gè) Compose 方法不可重啟。一般用于優(yōu)化簡單的 Compose 方法,這些方法內(nèi)部僅僅是調(diào)用其他 @Compose 方法,這樣可以避免冗余的 RestartGroup 與邏輯處理。在 Compose 內(nèi)部就有大量的場景使用。

有非 Unit 返回值的方法

如果一個(gè) Compose 方法存在非 Unit 的返回值,那這個(gè)方法也不能夠被重啟。因?yàn)檫@種方法的返回值通常是被調(diào)用方依賴,如果某次重組只重啟了該方法,那么調(diào)用方將無法感知該方法的返回值變更,可能造成預(yù)期外的 UI 異常。

open 方法

open 方法也無法被重啟,因?yàn)檫@類方法被 override 后會(huì)生成新的 RestartGroup,那么就會(huì)在一個(gè)方法中出現(xiàn)兩個(gè)RestartGroup,重組時(shí)可能發(fā)生異常。

內(nèi)聯(lián)方法的 Composable Lambda 參數(shù)

如果一個(gè) Composable Lambda 是作為 inline 方法的參數(shù),那么這個(gè) Composable Lambda 也無法被重組。最常見的是 Column、Box 等布局組件,這些組件均為 inline 方法,且接受一個(gè) Composable Lambda 作為參數(shù)。

在以下代碼中,uiState 關(guān)聯(lián)的重組作用域?yàn)?nbsp;ComposeUI,而不是 Column 或 Column 的尾 Lambda。

var uiState by mutableStateOf("UI State")

@Composable
fun ComposeUI(content: String) {
    Column {
        println(uiState)
    }
}

如果在這種場景下希望 ComposeLambda 能夠被重啟,可以為該參數(shù)添加 noinline 修飾符。

@Composable
inline fun RestartableColumn(noinline content: @Composable ColumnScope.() -> Unit) {
    Column { 
        content()
    }
}

跳過 Compose 方法執(zhí)行

雖然 Compose 會(huì)盡量限制重組范圍,但仍可能執(zhí)行一些無需更新的 Compose 方法。為避免這種非必要的執(zhí)行,Compose 編譯器會(huì)為 Compose 方法插入跳過邏輯,從而在無需更新時(shí)自動(dòng)跳過方法體執(zhí)行。

哪些 Compose 方法不可跳過

未開啟 Strong skipping mode 時(shí)編譯器還會(huì)判斷方法參數(shù)的穩(wěn)定性,以此決定是否為該方法生成跳過邏輯,但在 kotlin 2.0.20 后該功能默認(rèn)開啟,所以本文的原理分析均在該功能開啟的前提下進(jìn)行。

正如不是所有 Compose 方法都可以被重啟一樣,也不是所有 Compose 方法都可以被跳過。

我們可以通過閱讀 Compose 編譯器源碼來了解哪些方法無法被跳過,具體邏輯在 ComposableFunctionBodyTransformer#visitFunctionInScope 中,代碼如下:

總結(jié)下來就是「不可被重啟的方法同樣不可被跳過」,編譯器不會(huì)為不可重啟的方法生成 Skip 相關(guān)邏輯。

此外 Compose 還提供了 @NonSkippableComposable 注解,允許開發(fā)者手動(dòng)指定某個(gè) Compose 方法不可跳過。

如何跳過執(zhí)行

$changed 參數(shù)揭秘

編譯器首先會(huì)為 Compose 方法插入一個(gè)參數(shù) $changed,用于表示當(dāng)前方法各個(gè)參數(shù)的變化狀態(tài),為后續(xù)判斷是否能夠跳過重組提供輔助信息。

$changed 是 Int 類型,每三位保存一個(gè)參數(shù)的信息,最低位用來表示是否強(qiáng)制重組,因此一個(gè) $changed 能夠保存 10 個(gè)參數(shù)的信息。如果參數(shù)個(gè)數(shù)大于 10,那么就會(huì)添加 $changed1、$changed2,以此類推。整體結(jié)構(gòu)如下:

每個(gè)參數(shù)使用了 3 位來保存信息,其中低兩位用來表示參數(shù)是否變化,最高位表示當(dāng)前參數(shù)是否穩(wěn)定(Stable)。

參數(shù)變化信息有以下 4 種取值。

  • Uncertain (0b000):無法確定該參數(shù)較上一次重組是否有變化
  • Same (0b001):該參數(shù)較上一次重組沒有發(fā)生變化
  • Different (0b010):該參數(shù)較上一次重組發(fā)生了變化
  • Static (0b011):該參數(shù)為靜態(tài)對象,在 Compose 的生命周期內(nèi)不會(huì)發(fā)生變化

生成 $dirty 跳過執(zhí)行

Compose 方法是否跳過的判斷條件為「所有被使用的參數(shù)相比上一次重組均沒有發(fā)生變化」,所以 Compose 編譯器會(huì)結(jié)合 $changed 參數(shù)依次確認(rèn)每個(gè)參數(shù)是否變化,并最終決定是否跳過執(zhí)行。以一個(gè)簡單的例子來分析編譯生成的跳過邏輯。

@Composable
fun ComposeDemo(param1: Int, param2: Int) {
    println("$param1 $param2")
}

// 編譯后代碼
@Composable
fun ComposeDemo(param1: Int, param2: Int, $composer: Composer?, $changed: Int) {
  val $dirty = $changed
  // 判斷第一個(gè)參數(shù)是否是 Uncertain 0b000
  if ($changed and 0b0110 == 0) {
    // 通過 $composer.changed 來判斷參數(shù)是否發(fā)生變化,并更新 $dirty
    $dirty = $dirty or if ($composer.changed(param1)) 0b0100 else 0b0010
  }
  // 同樣的方式判斷第二個(gè)參數(shù)
  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 運(yùn)行時(shí)提供用于判斷參數(shù)是否變化的方法,不在本文討論范圍內(nèi)。

首先生成變量 $dirty 并賦值為 $changed,用于表示每個(gè)參數(shù)最終的變化狀態(tài)。

隨后會(huì)對每個(gè)參數(shù)進(jìn)行判斷,當(dāng)某個(gè)參數(shù)變化信息為 Uncertain 時(shí),會(huì)通過 Composer 來判斷參數(shù)是否發(fā)生變化,并更新$dirty。以第一個(gè)參數(shù)為例,當(dāng)$composer.changed返回 true 時(shí)會(huì)執(zhí)行 $dirty or 0b0100,也就是將 $dirty 中表示第一個(gè)參數(shù)狀態(tài)的第二位置為 1,從 Uncertain 變?yōu)?nbsp;Different,反之則是置為 Same。

完成所有參數(shù)校驗(yàn)后會(huì)判斷 $dirty and 0b00010011 != 0b00010010,如果為 true 則執(zhí)行方法,也就是說想要跳過執(zhí)行需要滿足 $dirty and 0b00010011 == 0b00010010。該判斷的含義為:

  • 最低位需要為 0,表示當(dāng)前并非強(qiáng)制重組,否則就需要執(zhí)行方法
  • 兩個(gè)參數(shù)的最低位都需要為 1,也就是兩個(gè)參數(shù)都是 Same 或 Static,邏輯上就是參數(shù)較上一次重組沒有發(fā)生變化

關(guān)于穩(wěn)定性請參考官方文檔

https://developer.android.com/develop/ui/compose/performance/stability

前面提到每個(gè)參數(shù)的最高位表示穩(wěn)定性,對于不穩(wěn)定的參數(shù) Compose 會(huì)采用不同的方法來判斷是否變化,由于上面的例子中參數(shù)均為編譯期可推斷的穩(wěn)定類型(Int),所以采用了 $composer.changed 來判斷。

如果我們將第二個(gè)參數(shù)類型改為編譯期無法推斷的類型,那么生成的邏輯將會(huì)有所變化。

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) { // 判斷參數(shù)穩(wěn)定性
      $composer.changed(param2)
    } else {
      $composer.changedInstance(param2)
    }
    ) 0b00100000 else 0b00010000
  }
  if ($dirty and 0b00010011 != 0b00010010 || !$composer.skipping) {
    println("$param1 $param2")
  } else {
    $composer.skipToGroupEnd()
  }
}

可以看到針對第二個(gè)參數(shù)首先會(huì)判斷最高位。

  • 如果是0則為穩(wěn)定類型,通過 $composer.changed 判斷,本質(zhì)上是通過==來比較重組前后的參數(shù)
  • 如果是1則為不穩(wěn)定類型,通過 $composer.changedInstance 判斷,本質(zhì)上是通過===來比較重組前后的參數(shù)

而對于未使用的參數(shù),Compose 編譯器也會(huì)非常智能的忽略它,減少不必要的運(yùn)算開銷。去掉 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 參數(shù),Compose 仍可以通過 $composer.changed 來判斷參數(shù)是否發(fā)生變化,也能夠正常實(shí)現(xiàn)跳過邏輯。但是 $composer.changed 是比較重的操作,如果 $changed 參數(shù)已經(jīng)提供了足夠的信息,那么就可以避免調(diào)用 $composer.changed,極大提升運(yùn)行時(shí)性能,這也是 $changed 的設(shè)計(jì)初衷。

下面我們來看一下 $changed 參數(shù)在各個(gè)場景下如何為 Compose 提供有效信息。

靜態(tài)參數(shù)信息

當(dāng)調(diào)用方遞靜態(tài)對象(同一個(gè)對象或值相同的基礎(chǔ)類型)作為參數(shù)時(shí),編譯器會(huì)將 $changed 對應(yīng)參數(shù)信息設(shè)置為 Static(011),這樣被調(diào)用的 Composable 方法就可以直接跳過這個(gè)參數(shù)的對比。

在下面的例子中, 編譯器識(shí)別出 ComposeScreen 傳入的參數(shù)為常量 1,所以傳遞 $changed 值 0b0110 將參數(shù)設(shè)置為 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()
  }
}

除了直接傳遞常量的場景外,我們也可以通過在方法或?qū)傩?/span>上標(biāo)注 @Stable 來幫助編譯器識(shí)別方法或?qū)傩缘闹凳欠袷庆o態(tài)對象,這種場景下 @Stable 的作用是告訴編譯器:

  • 方法:該方法的輸入不變時(shí),方法返回值也保持不變
  • 屬性:任意時(shí)刻該屬性的返回值保持不變

修改上面的例子,將參數(shù)改為調(diào)用 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()
  }
}

盡管是將方法的返回值作為參數(shù)傳遞,但編譯器仍然能夠識(shí)別到該參數(shù)為靜態(tài)參數(shù),就是因?yàn)?nbsp;stableFunction 被標(biāo)記為@Stable,且ComposeScreen調(diào)用 stableFunction 傳遞的是一個(gè)常量。

這種方法在 Compose 內(nèi)部也有普遍的使用,比如經(jīng)常作為參數(shù)使用的 Alignment。

同時(shí) Compose 編譯器也將一些常用的 Kotlin 標(biāo)準(zhǔn)庫方法視為 Stable,比如 listOf(1, 2, 3) 這樣的調(diào)用就會(huì)被認(rèn)為返回值是一個(gè)靜態(tài)對象,這些內(nèi)置的 Stable 方法在源碼中可以找到。

Compose 編譯器對靜態(tài)參數(shù)的識(shí)別還遠(yuǎn)不止于此,下表列出了 Compose 編譯器能夠識(shí)別的大部分場景。



場景





代碼塊





基礎(chǔ)類型常量





4





基礎(chǔ)類型常量運(yùn)算





(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 常量運(yùn)算





2 * 4.dp





@Immutable/@Stable+所有屬性都是 Static





KeyboardOptions(autoCorrect = false) 

PaddingValues(all = 16.dp)



參數(shù)變化信息

在某些場景下調(diào)用方會(huì)直接將自己的參數(shù)傳遞給下一個(gè) Composable 方法,由于該參數(shù)在調(diào)用方內(nèi)部已經(jīng)做過一次判斷,因此可以直接將判斷的結(jié)果通過$changed 傳遞下去,省去后面對該參數(shù)的判斷成本。

在下面的例子中,ComposeScreen 將自身的參數(shù) 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()
  }
}

處理默認(rèn)參數(shù)

Kotlin 支持方法參數(shù)的默認(rèn)值,原理上會(huì)在編譯期為方法添加一個(gè) $default 參數(shù)用于判斷某個(gè)參數(shù)是否使用默認(rèn)值,并在方法開頭為使用了默認(rèn)值的參數(shù)賦值。

而針對 Composable 函數(shù)中的參數(shù)默認(rèn)值,Compose 選擇了自己處理而不是交給 Kotlin 編譯器,因?yàn)樾枰幚砟J(rèn)值對跳過邏輯的影響,以一個(gè)簡單的例子看一下生成的代碼。

@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) {
    // 使用默認(rèn)值則設(shè)置為 Static
    $dirty = $dirty or 0b0110
  } elseif ($changed and 0b0110 == 0) {
    // 未使用默認(rèn)值正常判斷 changed
    $dirty = $dirty or if ($composer.changed(param)) 0b0100 else 0b0010
  }
  if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) {
    if ($default and 0b0001 != 0) {
      // 使用默認(rèn)值時(shí)為參數(shù)賦值
      param = 1
    }
    println(param)
  } else {
    $composer.skipToGroupEnd()
  }
}

和 Kotlin 默認(rèn)參數(shù)處理思路是一樣的,處理流程為:

  • 為方法增加 $default 參數(shù),每一位表示對應(yīng)參數(shù)是否使用默認(rèn)值
  • 如果參數(shù)使用了默認(rèn)值,則設(shè)置 $dirty 設(shè)置為 Static,跳過判斷
  • 如果參數(shù)未使用默認(rèn)值,則正常走 changed 判斷
  • 如果最終無法跳過當(dāng)前 Composable 執(zhí)行,則為使用了默認(rèn)值的參數(shù)賦值

看到這個(gè)代碼不由得會(huì)產(chǎn)生一個(gè)疑問:為什么 param 一旦使用默認(rèn)值,就可以被判定為 Static ?如果上一次組合調(diào)用 DefaultTest 沒用默認(rèn)值,而這次重組用了默認(rèn)值,那么 param 不就發(fā)生變化了嗎?

其實(shí)仔細(xì)想想就可以理解:如果某次重組時(shí) param 使用了默認(rèn)值,那么在整個(gè) Composition 周期內(nèi)它必然始終都會(huì)使用默認(rèn)值。這是由調(diào)用點(diǎn)在編譯期就決定的,一旦出現(xiàn)非默認(rèn)值的情況,就意味著調(diào)用點(diǎn)發(fā)生了變化,兩次調(diào)用本質(zhì)上已不再屬于同一個(gè) Compose UI。

不過在這個(gè)例子中默認(rèn)值是 1,前面介紹過對于這種常量 Compose 能夠識(shí)別為 Static 對象,如果我們將默認(rèn)值改為一個(gè)方法調(diào)用會(huì)發(fā)生什么?

@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 中參數(shù)對應(yīng)信息設(shè)置為 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 默認(rèn)值改為 getInt 調(diào)用后,由于 Compose 無法推測該調(diào)用是否是 Static ,所以 $dirty 生成的策略有所變化

  1. 優(yōu)先檢查 $changed
  • 如果已有參數(shù)變化信息,直接使用,無需額外判斷。
  1. 若無變化信息,則判斷默認(rèn)值使用情況
  • 使用了默認(rèn)值,則設(shè)置為 Same。
  • 未使用默認(rèn)值,按常規(guī)通過 changed 判斷。

此外,在后續(xù)無法跳過執(zhí)行需要為參數(shù)賦值時(shí),Compose 還會(huì)增加一段邏輯:將 $dirty 中參數(shù)對應(yīng)信息設(shè)置為 Uncertaion(0b000)。

這么做的原因是:雖然使用默認(rèn)值時(shí)被標(biāo)記為了 Same,但由于這類調(diào)用不是 Static,Compose 實(shí)際上無法保證其是否真的沒有發(fā)生變化。為了避免對子 Composable 的判斷產(chǎn)生誤導(dǎo),最終將其標(biāo)記為 Uncertain,從而強(qiáng)制子 Composable 重新進(jìn)行判斷。在源碼中也可以看到官方的解釋。

如何處理 Composable Lambda

上面討論的場景以及例子都是針對普通 @Composable 方法,而對于 Composable Lambda 的處理稍有不同。Compose 編譯器會(huì)將 Composable Lambda 分為三類,并采用不同的處理策略。

無法跳過執(zhí)行的Composable Lambda

需要注意的是,@NonRestartableComposable、@NonSkippableComposable 對 Lambda 無效。

這部分前面已經(jīng)介紹過,如果 Composable Lambda 有返回值或者是作為 inline 方法的參數(shù),那么該 Composable Lambda 則無法跳過執(zhí)行,編譯器不會(huì)做任何的優(yōu)化。

@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()
  }
}

可跳過執(zhí)行的Composable Lambda

對于可正常跳過執(zhí)行的 Composable Lambda,編譯器會(huì)對其進(jìn)行一層封裝,具體封裝邏輯取決于該 Lambda 是否捕獲外部變量。

不捕獲外部變量

在 Kotlin 中,一個(gè)不捕獲外部變量的 Lambda 最終會(huì)被優(yōu)化為一個(gè)單例,因?yàn)檫@種 Lambda 沒有任何狀態(tài),優(yōu)化為單例對邏輯沒有任何影響且能夠節(jié)省運(yùn)行開銷。

類似的,針對不捕獲外部變量的 Composable Lambda,Compose 編譯器也會(huì)為期生成一個(gè)單例,同時(shí)通過 composableLambdaInstance 進(jìn)行封裝。

@Composable
fun TestComposeLambda() {
    // 無狀態(tài) 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 捕獲了外部變量,則無法優(yōu)化為單例。這種情況下 Compose 會(huì)使用 remember 來緩存該 Composable Lambda 對象,避免每次重組都會(huì)創(chuàng)建新的 Lambda 實(shí)例。

@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實(shí)際上是基于 remember 創(chuàng)建 Lambda 對象。



責(zé)任編輯:龐桂玉 來源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2023-03-26 20:39:01

2010-03-23 11:17:16

Python 動(dòng)態(tài)編譯

2010-10-20 13:43:37

C++編譯器

2022-05-18 09:31:42

編譯器開源代碼生成

2010-01-18 10:34:21

C++編譯器

2010-01-12 16:42:59

C++編譯器

2010-01-21 09:11:38

C++編譯器

2017-03-20 18:01:55

編譯器匯編

2009-08-10 17:12:54

C#編譯器

2013-03-29 10:02:37

編譯器語言編譯開發(fā)

2009-07-07 09:14:53

Milepost GC編譯器

2017-07-24 13:13:00

智能AICIO

2010-01-14 16:46:13

CentOS Mysq

2019-08-06 08:20:07

編譯器工具開發(fā)者

2017-04-07 11:12:22

智能黑科技汽車

2013-12-30 11:21:31

Go編譯器

2010-02-02 17:08:26

Python靜態(tài)編譯器

2010-03-02 10:55:47

Linux SkyEy

2010-09-16 15:57:25

Java編譯器

2009-08-06 14:59:36

C#編譯器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號