前言
Jetpack Compose 何时重构 UI 的核心是 State
。在 Compose 的状态管理中,使用 MutableState
保存一个可以更改的状态值,一旦值改变就会触发重组。
在使用 Jetpack Compose 时,我们经常使用 mutableStateOf
函数来初始化的新 MutableState
实例,如下所示:
var domainName by remember { mutableStateOf("https://itmob.cn") }
如下代码我们定义了一个 Composable 可组合函数,并定义了一个 MutableState
对象用于保存状态
@Composable
private fun SnapshotMutationPolicySample() {
var domainName by remember { mutableStateOf("https://itmob.cn") }
Button(
onClick = {
domainName = "https://itmob.cn"
},
) {
Text(text = domainName)
Log.i("Composable State", "recomposition / 重组")
}
}
// 点击按钮时没有任何日志输出
上面的可组合函数,点击按钮时在 onClick 函数中为 domainName 重新赋值,但并没有输出任何日志,也就是说并不会触发 Button 的重组。
这是因为在
MutableState
出现新的值时,默认会检查新值与旧值是否在结构上相等(==),如果新旧两个值相等,则不视为更改。这个过程就是mutableStateOf
默认的快照策略
也就是 domainName 重新赋的值跟前值相同,所以状态并没有变化,不会触发 Button 的重组。
什么是 SnapshotMutationPolicy
上面的例子中 mutableStateOf
是怎样判断新赋的值跟旧值相同而不会触发状态改变的呢?
我们来看看 mutableStateOf
函数的源码:
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
可以看到 mutableStateOf
还有第二个参数 policy,它是 SnapshotMutationPolicy
类型。
文档中 SnapshotMutationPolicy
的介绍:
A policy to control how the result of mutableStateOf report and merge changes to the state object.
意思是 SnapshotMutationPolicy
是一种快照冲突时的解决策略,通过它可以控制如何将一个状态视为已更改。
这是 SnapshotMutationPolicy
接口的定义:
@JvmDefaultWithCompatibility
interface SnapshotMutationPolicy<T> {
/**
* 确定设置的新旧状态值是否应视为相等。如果返回 true,则新值不被视为更改
*/
fun equivalent(a: T, b: T): Boolean
/**
* 合并快照中冲突的更改
*/
fun merge(previous: T, current: T, applied: T): T? = null
}
我们可以看到该接口中定义了两个函数,这里我们先只看 equivalent
,merge
函数将在之后的文章中介绍。
equivalent
是如何判断新值是否被视为状态更改的函数。所以实现 SnapshotMutationPolicy
接口就可以控制 State/状态的值改变时是否视为状态改变,从而影响可组合项是否重组。
这种策略可以作为参数传递给
mutableStateOf
和compositionLocalOf
,提供判断状态是否更新的策略
标准库中预定义的策略
Jetpack Compose 的标准库带有三个预定义的快照策略:
-
StructuralEqualityPolicy
如下是
StructuralEqualityPolicy
的源码:@Suppress("UNCHECKED_CAST") fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> = StructuralEqualityPolicy as SnapshotMutationPolicy<T> private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> { override fun equivalent(a: Any?, b: Any?) = a == b override fun toString() = "StructuralEqualityPolicy" }
从
StructuralEqualityPolicy
的定义可以看出,这个策略是通过==
操作符对比新旧值的结构来判断状态是否改变的。而且StructuralEqualityPolicy
是mutableStateOf
函数中 policy 参数的默认值。这也就是为什么前言中给出的例子里点击按钮对状态重新赋值并不会触发重组的原因,因为
==
操作符等价于equals
用于比较两个对象的结构是否相等。 -
ReferentialEqualityPolicy
如下是
ReferentialEqualityPolicy
的源码:@Suppress("UNCHECKED_CAST") fun <T> referentialEqualityPolicy(): SnapshotMutationPolicy<T> = ReferentialEqualityPolicy as SnapshotMutationPolicy<T> private object ReferentialEqualityPolicy : SnapshotMutationPolicy<Any?> { override fun equivalent(a: Any?, b: Any?) = a === b override fun toString() = "ReferentialEqualityPolicy" }
可以看出该策略是通过
===
操作符来对比新旧状态值的,也就是比较两个值的引用(内存地址)是否相同。使用
ReferentialEqualityPolicy
策略时,新旧值的引用/内存地址不同即作为状态改变。因此我们需要根据引用是否改变来判断是否重组时,可以使用此策略 -
NeverEqualPolicy
如下是
NeverEqualPolicy
的源码:@Suppress("UNCHECKED_CAST") fun <T> neverEqualPolicy(): SnapshotMutationPolicy<T> = NeverEqualPolicy as SnapshotMutationPolicy<T> private object NeverEqualPolicy : SnapshotMutationPolicy<Any?> { override fun equivalent(a: Any?, b: Any?) = false override fun toString() = "NeverEqualPolicy" }
源码中无论新旧值是什么
equivalent
的返回值总是 false,也就是无论新旧值是怎样的都作为不同的值,也就是使用此策略时每次更新状态的值都视为更改
从三种预定义的策略的源码可用看到,它们都被定义成了私有的
object
类型,而且定义了相应的公开方法用于获取它们。
示例
下面我们通过示例看看三种策略的用法和区别:
1. StructuralEqualityPolicy
data class Domain(val id: Int, var name: String, val url: String)
@Composable
private fun StructuralEqualityPolicySample() {
var domain by remember {
mutableStateOf(
value = Domain(
id = 0,
name = "itmob",
url = "https://itmob.cn",
),
)
}
Button(onClick = {}) {
Text(text = "StructuralEqualityPolicy")
println("Domain instance with StructuralEqualityPolicy: $domain")
}
LaunchedEffect(Unit) {
delay(1000)
domain = domain.copy(name = "New name")
delay(1000)
domain = domain.copy(name = "New name")
}
}
/**
* 输出:
* 第一次渲染,输出 domain 的值
* Domain instance with StructuralEqualityPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* LaunchedEffect 中修改了字段 name 并赋新值,domain 对象发生了变化
* Domain instance with StructuralEqualityPolicy: Domain(id=0, name=New name, url=https://itmob.cn)
* LaunchedEffect 中再次修改字段 name 并赋新值,但 domain 对象没有变化,不重组不输出任何内容
*/
默认情况下 mutableStateOf
函数的 policy 参数的默认值是 StructuralEqualityPolicy
,
2. ReferentialEqualityPolicy
@Composable
private fun ReferentialEqualityPolicySample() {
var domain by remember {
mutableStateOf(
value = Domain(
id = 0,
name = "itmob",
url = "https://itmob.cn",
),
policy = referentialEqualityPolicy(),
)
}
Button(onClick = {}) {
Text(text = "ReferentialEqualityPolicy")
println("Domain instance with ReferentialEqualityPolicy: $domain")
}
LaunchedEffect(Unit) {
delay(1000)
domain = domain.copy(name = "itmob")
delay(1000)
domain = domain.copy(name = "itmob")
}
}
/**
* 输出:
* Domain instance with ReferentialEqualityPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* Domain instance with ReferentialEqualityPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* Domain instance with ReferentialEqualityPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
*/
使用 ReferentialEqualityPolicy
时,无论 domain 对象的字段是否变化,只要给它赋的新值的引用(内存地址)发生变化,就会作为新触发可组合项重组。
3. NeverEqualPolicy
@Composable
private fun NeverEqualPolicySample() {
var domain by remember {
mutableStateOf(
value = Domain(
id = 0,
name = "itmob",
url = "https://itmob.cn",
),
policy = neverEqualPolicy(),
)
}
Button(onClick = {}) {
Text(text = "NeverEqualPolicy")
println("Domain instance with NeverEqualPolicy: $domain")
}
LaunchedEffect(Unit) {
delay(1000)
domain = domain
delay(1000)
domain = domain
}
}
/**
* 输出:
* Domain instance with NeverEqualPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* Domain instance with NeverEqualPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* Domain instance with NeverEqualPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
*/
使用 NeverEqualPolicy
时,无论给 domain 赋的新值的结构和引用是否发生改变,只要赋值就视为状态改变而触发可组合项重组。
所以本例中即使给 domain 赋值其本身,domain 对象的字段和引用都没有变化,依然会触发 Button 可组合项的重组。
4. 自定义策略
@Composable
private fun CustomEqualPolicySample() {
var domain by remember {
mutableStateOf(
value = Domain(
id = 0,
name = "itmob",
url = "https://itmob.cn",
),
// 自定义 policy
policy = object : SnapshotMutationPolicy<Domain> {
override fun equivalent(a: Domain, b: Domain) = a.id == b.id
},
)
}
Button(onClick = {}) {
Text(text = "CustomEqualPolicy")
println("Domain instance with CustomEqualPolicy: $domain")
}
LaunchedEffect(Unit) {
delay(1000)
domain = domain.copy(id = 1)
delay(1000)
domain = domain.copy(name = "New name")
}
}
/**
* 输出:
* Domain instance with CustomEqualPolicy: Domain(id=0, name=itmob, url=https://itmob.cn)
* Domain instance with CustomEqualPolicy: Domain(id=1, name=itmob, url=https://itmob.cn)
*/
上面的代码中自定义了我们自己的 policy 并实现了 equivalent
方法,只要新旧两个值的 id 相同则视为它们没有变化。
所以 LaunchedEffect
中第一次给状态重新赋值时修改了 id 则触发了 Button 的重组,而第二次重新赋值,只修改了 domain 的 name 字段,则不会触发 Button 的重组。
总结
本文介绍了 MutableState
是怎样判断状态是否需要更新的,因此,如果正确使用预定义的 SnapshotMutationPolicy
或自定义 SnapshotMutationPolicy
可以避免由于无意义的更新而导致不必要的重组。