主题寄语:心理学家发现绿色能让心情放轻松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
复制代码