主題寄語:心理學家發現綠色能讓心情放輕鬆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
。。。。性能
你能夠在文章末尾找到答案,與你的判斷是否一致呢?學習
Compose 在編譯期分析出會受到某 state
變化影響的代碼塊,並記錄其引用,當此 state 變化時,會根據引用找到這些代碼塊並標記爲 Invalid
。在下一渲染幀到來以前 Compose 會觸發 recomposition,並在重組過程當中執行 invalid 代碼塊。測試
Invalid 代碼塊即編譯器找出的下次重組範圍。可以被標記爲 Invalid 的代碼必須是非 inline 且無返回值的 @Composalbe function/lambda
,必須遵循 重組範圍最小化 原則。
對於 inline 函數,因爲在編譯期會在調用處中展開,所以沒法在下次重組時找到合適的調用入口,只能共享調用方的重組範圍。
而對於有返回值的函數,因爲返回值的變化會影響調用方,所以沒法單獨重組,而必須連同調用方一同參與重組,所以它不能做爲入口被標記爲 invalid
只有會受到 state 變化影響的代碼塊纔會參與到重組,不依賴 state 的代碼不參與重組。
在瞭解 Compose 重繪範圍的基本規則以後,咱們再回看文章開頭的例子,並嘗試回答下面的問題:
當點擊 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 中沒有任何對 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 的調用方 Foo 不參與重組,Button 天然也不會參與重組,只有尾 lambda 參與重組便可。
重組範圍必須是 @Composable 的 function/lambda ,onClick 是一個普通 lambda,所以與重組邏輯無關。
前面講了,只有 非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
複製代碼
Column
、Row
、Box
乃至 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
複製代碼