Compose 的重組會影響性能嗎?聊一聊 recomposition scope

主題寄語:心理學家發現綠色能讓心情放輕鬆android

很多初學 Compose 的同窗都會對 Composable 的 Recomposition(官方文檔譯爲"重組")心生顧慮,擔憂大範圍的重組是否會影響性能。安全

其實這種擔憂大可沒必要, Compose 編譯器在背後作了大量工做來保證 recomposition 範圍儘量小,從而避免了無效開銷:markdown

Recomposition skips as much as possible
When portions of your UI are invalid, Compose does its best to recompose just the portions that need to be updated.
developer.android.com/jetpack/com…app

那麼當重組發生時,其代碼執行的範圍到底是怎樣的呢?咱們經過一個例子來測試一下:ide

@Composable
fun Foo() {
    var text by remember { mutableStateOf("") }
    Log.d(TAG, "Foo")
    Button(onClick = {
        text = "$text $text"
    }.also { Log.d(TAG, "Button") }) {
        Log.d(TAG, "Button content lambda")
        Text(text).also { Log.d(TAG, "Text") }
    }
}
複製代碼

如上,當點擊 button 時,State 的變化會觸發 recomposition。函數

請你們思考一下此時的日誌輸出是怎樣的oop

image.png 。。。。性能

你能夠在文章末尾找到答案,與你的判斷是否一致呢?學習


Compose 如何肯定重組範圍?

Compose 在編譯期分析出會受到某 state 變化影響的代碼塊,並記錄其引用,當此 state 變化時,會根據引用找到這些代碼塊並標記爲 Invalid 。在下一渲染幀到來以前 Compose 會觸發 recomposition,並在重組過程當中執行 invalid 代碼塊。測試

Invalid 代碼塊即編譯器找出的下次重組範圍。可以被標記爲 Invalid 的代碼必須是非 inline 且無返回值@Composalbe function/lambda,必須遵循 重組範圍最小化 原則。

爲什麼是 非 inline 且無返回值(返回 Unit)?

對於 inline 函數,因爲在編譯期會在調用處中展開,所以沒法在下次重組時找到合適的調用入口,只能共享調用方的重組範圍。

而對於有返回值的函數,因爲返回值的變化會影響調用方,所以沒法單獨重組,而必須連同調用方一同參與重組,所以它不能做爲入口被標記爲 invalid

範圍最小化原則

只有會受到 state 變化影響的代碼塊纔會參與到重組,不依賴 state 的代碼不參與重組。

在瞭解 Compose 重繪範圍的基本規則以後,咱們再回看文章開頭的例子,並嘗試回答下面的問題:


爲何不僅是 Text 參與重組?

當點擊 button 後,MutableState 發生變化,代碼中惟一訪問這個 state 的地方是 Text(...) ,爲何重組範圍不僅是 Text(...) ,而是 Button {...} 的整個花括號?

首先要理解出如今 Text(...) 參數中的 text 其實是一個表達式

下面兩中寫法在執行順序上是等價的

println(「hello」 + 「world」)
複製代碼
val arg = 「hello」 + 「world」
println(arg)
複製代碼

老是 「hello」 + 「world」 做爲表達式先執行,而後纔是 println 方法的調用。

回到前面的例子,參數 text 做爲表達式執行的調用處是 Button 的尾lambda,然後才做爲參數傳入 Text()。 因此此時最小重組範圍是 Button 的 尾lambda 而非 Text()


Foo 是否參加劇組 ?

按照範圍最小化原則, Foo 中沒有任何對 state 的訪問,因此很容易知道 Foo 不該該參與重組。

有一點須要注意的是,例子中 Foo 經過 by 的代理方式聲明 text,若是改成 = 直接爲 text 賦值呢?

@Composable fun Foo() {
  val text: MutableState<String> = remember { mutableStateOf("") }

  Button(onClick = { 
  	 text = "$text $text"
  }) {
    Text(text.value)
  }
}
複製代碼

答案是同樣的,仍然不會參與重組。

第一,Compose 關心的是代碼塊中是否有對 state 的 read,而不是 write

第二,這裏的 = 並不意味着 text 會被賦值新的對象,由於 text 指向的 MutableState 實例是永遠不會變的,變的只是內部的 value


爲何 Button 不參與重組?

這個很好解釋,Button 的調用方 Foo 不參與重組,Button 天然也不會參與重組,只有尾 lambda 參與重組便可。


Button 的 onClick是否參與重組?

重組範圍必須是 @Composable 的 function/lambda ,onClick 是一個普通 lambda,所以與重組邏輯無關。


注意!重組中的 Inline 陷阱!

前面講了,只有 非inline函數 纔有資格成爲重組的最小範圍,理解這點特別重要!

咱們將代碼稍做改動,爲 Text() 包裹一個 Box{...}

@Composable
fun Foo() {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Box {
            Log.d(TAG, "Box")
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}
複製代碼

日誌以下:

D/Compose: Button content lambda
D/Compose: Boxt
D/Compose: Text
複製代碼

爲何重組範圍不是從Box開始?

ColumnRowBox 乃至 Layout 這種容器類 Composable 都是 inline 函數,所以它們只能共享調用方的重組範圍,也就是 Button 的 尾lambda

若是你但願經過縮小重組範圍提升性能怎麼辦?

@Composable
fun Foo() {

    var text by remember { mutableStateOf("") }

	Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}

@Composable
fun Wrapper(content: @Composable () -> Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}
複製代碼

如上,自定義非 inline 函數,使之知足 Compose 重組範圍最小化條件。


結論

Just don't rely on side effects from recomposition and compose will do the right thing -- Compose Team

關於重組範圍的具體規則,官方文檔中沒有作詳細說明。由於開發者只須要牢記 Compose 經過編譯期優化保證了recomposition 永遠按照最合理的方式運行,以最天然的方式開發就行了,無需針對這些具體規則付出額外的學習成本。

儘管如此,做爲開發者仍要謹記一點:

不要直接在 Composable 中寫包含反作用(SideEffect)的邏輯!

反作用不能跟隨 recomposition 反覆執行,因此咱們須要保證 Composable 的「純潔性」。

你不能預設某個 function/lambda 必定不參與重組,於是在裏面僥倖的埋了一些反作用代碼,使其變得不純潔。由於咱們沒法肯定這裏是否存在 「inline陷阱」,即便能肯定也不保證如今的優化規則在將來不會改變。

因此最安全的作法是,將反作用寫到 LaunchedEffect{}DisposableEffect{}SideEffect{} 中,而且使用 remeber{}derivedStateOf{} 處理那些耗時的計算。



開頭例子的運行結果:

D/Compose: Button content lambda
D/Compose: Text
複製代碼
相關文章
相關標籤/搜索