Jekpack Compose “状态订阅&自动刷新” 系列:
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - MutableState/mutableStateOf 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - remember 和重组作用域 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - 有状态、无状态、状态提升?】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - mutableStateListOf 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - 你真的了解重组吗?】
先考虑一个问题,什么时候会重组?
val name by remember { mutableStateOf("Hi Compose")}
val nums = mutableStateListOf(1, 2, 3)
val maps = mutableStateMapOf(1 to "One", 2 to "Two")
通过之前的几篇文章,你应该对这三行代码不陌生,当用 mutableState*Of 申明一个变量的时候,在可组合项中,如果变量变化了,就会触发重组。
比如下面代码:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {Column {Text(name, Modifier.clickable { name = "Hi, Kotlin" })}}}
}
运行:
Text(name) 被重组了,但是并不是仅仅 Text(name) 被重组,而是整个 Lambda 表达式,我们之前说过,这就是重组作用域。
那么思考一个问题:
我们先看一个好玩的东西,查看 Column 函数:
@Composable
inline fun Column(modifier: Modifier = Modifier,verticalArrangement: Arrangement.Vertical = Arrangement.Top,horizontalAlignment: Alignment.Horizontal = Alignment.Start,content: @Composable ColumnScope.() -> Unit
) {val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)Layout(content = { ColumnScopeInstance.content() },measurePolicy = measurePolicy,modifier = modifier)
}
Column 是个 inline 函数(内联函数),这就意味着在实际编译之前,这个函数的调用会被替换成内部实际的代码,比如会是下面这个样子:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {// Column {// 替换为Layout(Text(name, Modifier.clickable { name = "Hi, Kotlin" }))// }}}
}
再来看下 Layout():
@Composable inline fun Layout(content: @Composable @UiComposable () -> Unit,modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
) {... ...ReusableComposeNode<ComposeUiNode, Applier<Any>>(... ...content = content)
}
也是个 inline 函数,那么代码又回变成这样:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {// Column {// Layout(// 替换为ReusableComposeNode(Text(name, Modifier.clickable { name = "Hi, Kotlin" }))// )// }}}
}
再来看下 ReusableComposeNode:
@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ReusableComposeNode(noinline factory: () -> T,update: @DisallowComposableCalls Updater<T>.() -> Unit,noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,content: @Composable () -> Unit
) {... ...content()... ...
}
又是 inline 函数,而且你看它内部干啥了?直接调用了 content()
!
所以最终代码在编译前其实会把 Column {}
给拿掉,就跟下面一样:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {// Column {// Layout(// 替换为// ReusableComposeNode(Text(name, Modifier.clickable { name = "Hi, Kotlin" })// )// )// }}}
}
那么这就意味着:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {// 组件 1,这个地方会因为 name 的改变而一起重新调用Column {// 组件 2,这个地方会因为 name 的改变而一起重新调用Text(name, Modifier.clickable { name = "Hi, Kotlin" })// 组件 3,这个地方会因为 name 的改变而一起重新调用}// 组件4,这个地方会因为 name 的改变而一起重新调用}}
}
为了验证,我们测试下:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {println("Recompose 范围测试 1")Column {println("Recompose 范围测试 2")Text(name, Modifier.clickable { name = "Hi, Kotlin" })println("Recompose 范围测试 3")}println("Recompose 范围测试 4")}}
}
运行:
这个 Log 没有任何问题,现在我们点击:
Log 已经验证了我们刚才的结论,那这就涉及到了一个问题:性能风险!
比如下面的代码有可能出现在你的代码中:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {println("Recompose 范围测试 1")Column {println("Recompose 范围测试 2")Text(name, Modifier.clickable { name = "Hi, Kotlin" })println("Recompose 范围测试 3")}NBFunction()println("Recompose 范围测试 4")}}
}@Composable
fun NBFunction() {println("Recompose 范围测试:我干了很多消耗性能的事~~~")// 这里面干了很多 NB 的事,逻辑量特别大
}
你写了一个很牛逼的 Composable 函数,里面干了很多复杂的事,特别消耗性能,那么每次随着 name 的更改,NBFunction() 都会被执行一遍,就就会存在巨大的性能消耗。
你要不信,我们运行一下,看下 Log:
🤔???点击 name 后,NBFunction() 竟然没有再次执行?但其他 Log 仍然执行了啊!
难道 NBFunction() 没有被调用?
其实 NBFunction() 被调用了,只不过进入 NBFunction() 内部后,内部的代码没有被执行。
🤔???都进来了,你跟我说内部代码不执行?-- 对!
其实这是因为:在 Compose 的编译过程中,编译器插件会做干预,这个干预过程会修改我们的 Compose 函数,比如说它会给函数内部的代码加上一些条件判断,判断这个函数的参数跟上一次函数被调用的时候传入的参数有没有改变,如果没有改变,就直接跳过这个函数的内部代码的执行,这是 Compose 的优化。
而你看 NBFunction() 这个函数有参数吗?-- 没有,所以它内部代码永远不会执行,所以 Log 肯定不会打印出来。
来试一下,改下代码:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")setContent {println("Recompose 范围测试 1")Column {println("Recompose 范围测试 2")Text(name, Modifier.clickable { name = "Hi, Kotlin" })println("Recompose 范围测试 3")NBFunction(name)}println("Recompose 范围测试 4")}}
}@Composable
fun NBFunction(name: String) {println("Recompose 范围测试:我干了很多消耗性能的事~~~")// 这里面干了很多 NB 的事,逻辑量特别大Text("$name")
}
我们给 NBFunction 加个参数,那么随着 name 的改变,看看它会不会被执行:
结果显而易见了!Compose 重组过程中会判断 NBFunction() 的参数 String 是否变化,那么如果我的参数是一个对象呢?
比如我传入的是:
data class User(val name: String)
Compose 在重组过程中,依然会对对象类型的参数做判断,不过它的判断规则是 Kotlin 中的 “==”,等同于 Java 的 “equals”,是结构性相等判断。
现在我们修改下代码:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)var name by mutableStateOf("Hi, Compose")var user = User("marco") // User 对象setContent {println("Recompose 范围测试 1")Column {println("Recompose 范围测试 2")Text(name, Modifier.clickable {name = "Hi, Kotlin"user = User("marco") // 点击后,新的 User 对象})println("Recompose 范围测试 3")NBFunction(user)}println("Recompose 范围测试 4")}}
}@Composable
fun NBFunction(user: User) {println("Recompose 范围测试:我干了很多消耗性能的事~~~")// 这里面干了很多 NB 的事,逻辑量特别大Text("${user.name}")
}data class User(val name: String)
代码很简单,我们通过 Log 验证下:
我们再改下代码,把 User 换成一个新的内容:
var user = User("marco")Text(name, Modifier.clickable {name = "Hi, Kotlin"user = User("marco_2")
})
再看下 Log:
确实如我们前面预想一样,Compose 重组过程中同样会对 NBFunction() 的对象参数也做判断,是结构性相等判断。
接下来我们看个有意思的东西,我们再来改一下代码:
var user = User("marco")Text(name, Modifier.clickable {name = "Hi, Kotlin"user = User("marco"). // user 改回来,都是 marco 内容
})==> 此时肯定是会判断相等,不会执行 NBFunction() 内部的代码
---
==> 接下来我们只改一下关键字:
data class User(val name: String)
// val --> var
data class User(var name: String)
仅仅把 val
改成 var
,运行看 Log:
🤔???val 就会跳过 NBFunction() 内部代码,var 就不会跳过 NBFunction() 内部代码?Why?
这是因为 Compose 的本身机制设定:
data class User(val name: String) // Compose 会认为这是一个可靠的类
data class User(var name: String) // Compose 会认为这是一个不可靠的类
对于不可靠的类,我就不管你了,直接进!这是因为出于界面正确性的考虑。
比如我们修改下代码:
所以,与性能相比,准确性才是最终要的,所以就会无条件的进入 NBFunction() 函数再执行一遍。
那如果假设我们可以保证不会出现以上情况,保证 User 对象永远相等,希望 Composable 插件也可以跳过内部执行,提升性能,如何做呢?
用 @Stable 注解,它是一个稳定性注解,告诉 Compose 编译器插件,这个类是可靠的。这样 Compose 重组过程中就会跳过 NBFunction() 内部代码。
@Stable
data class User(var name: String)
运行看下 Log:
除了 @Stable 可以认定可靠以外,还有一种方式可以告诉 Compose 是可靠的:
class User(name: String) {var name by mutableStateOf(name) // 这是一种更通用的写法
}