「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
在 Compose 中显示数据,通常我们要将其存储在变量中,并用 mutableStateOf 包裹,以便实现自动监听与更新。这一步有下列三种写法:
val name = mutableStateOf("Bob") // 1
val name by mutableStateOf("Bob") // 2
val name by remember { mutableStateOf("Bob") } // 3
它们之间有什么区别?如何选择?这节先来解决最基本的问题 —— mutableStateOf() 究竟做了什么?
Compose 过程
在深挖之前,有必要先了解 Compose 从构建到显示的完整过程。
Compose 分为三个步骤:组合 (compose)、布局、绘制。前者是 Compose 独有的,后面两个与传统 View 类似。所谓「组合」,就是根据我们写的代码创建出实际的界面,组合的结果称为 Composition。
插一句题外话,Compose 函数虽然看起来很像是创建一个对象,例如
Text(text = name.value)很像创建一个Text对象,但实际上不是这样。跟踪源码可以发现,这只不过是一个普通的函数,它没有返回值,因此类似Text().text = name.value的写法完全错误 ❌ 这与传统 View 中TextView().text = "xxx"不是一回事。—— 所以需要「组合」
如果还是无法理解,可以将「组合」类比传统 View 中 inflat,解析 xml 文件创建实际的对象。只不过曾经我们可以绕过 xml 手动创建 view,而现在不行了。
MutableState
最基本的用例大概是这样:
val name = mutableStateOf("Bob")
setContent {
Column {
Text(text = name.value)
Button(onClick = { name.value = "2" }) {
Text(text = "Change Name")
}
}
}
定义了一个 Text 和 Button,点击按钮,显示的文本就会改变。
跟踪源码轻易看出,mutableStateOf() 就是创建一个 ParcelableSnapshotMutableState 对象然后返回它。ParcelableSnapshotMutableState 本身并没有什么有价值的东西,主要是对 parcelable 的实现便于进程通信。真正储存数据的是它的父类 SnapshotMutableStateImpl:
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
private var next: StateStateRecord<T> = StateStateRecord(value)
override val firstStateRecord: StateRecord
get() = next
// 省略其他的 ...
}
显然 value 就是保存实际数据的东西,可惜它依然不是最终实现。继续探索便遇到一个关键类 StateRecord (StateStateRecord 是它的子类),要搞清楚它,首先要看看 SnapshotMutableStateImpl 所实现的两个接口。现在我们已经知道 mutableStateOf() 可以返回一个容器对象,它能够被订阅并且在内部值改变时通知订阅者,以此实现 UI 自动刷新。「被订阅」这种特性其实是 StateObject 接口提供的,很反直觉。
再次套娃🪆,StateObject 本身又是一个容器,以链表的形式存储着一串 StateRecord ⬅️ 这个真正保存了数据。为什么是一串?因为它不仅保存了变量的最新值,还保留了曾经的值,以便实现「撤销」等功能。既然是链表,那肯定存在第一个节点,就是 StateObject.firstStateRecord ,后继节点就是 StateRecord.next。注意这个 next 和 SnapshotMutableStateImpl.next 无关,恰好重名而已。事实上 SnapshotMutableStateImpl 里有一个链表保存着所有值(实现了 StateObject 接口呀),它的首个节点是 SnapshotMutableStateImpl.next。SnapshotMutableState
小结:StateObject 以链表形式真正储存了数据,StateRecord 是链表的节点,SnapshotMutableStateImpl 是一个实现。
get
get 操作调用 readable 取得 StateRecord 进而取得其内部包装的实际值返回。
readable() 有三个重载版本。每当取值的时候会调用单参数版本,内部调用双参数版本,最终调用三参数版本获取真正的值。
- 单参数:双参数的便捷函数
- 双参数:记录本次使用(订阅)
- 三参数:遍历 StateRecord 链表,找到最新的、可用的 StateRecord
「最新」的好理解,「有效」后面再深入
双参数的函数是自动刷新 UI 的核心。确保每次取值的时候,这个动作都会被记录,后边值有变化的时候就可以更新了。
set
设置值时先套了一层 withCurrent,这个函数比较简单,就是调用三参数的 readable() 找到最新可用的 StateRecord,作为执行 lambda 表达式的参数:
next.withCurrent { it: StateStateRecord<T> ->
// it 就是最新可用的 StateRecord
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
lambda 判断若新旧值不相等则执行赋值操作。与取值对应,我们猜测 overwritable 内实现了通知机制。具体的跟踪进去看看:
internal inline fun <T : StateRecord, R> T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block()
}.also {
notifyWrite(snapshot, state)
}
}
internal fun <T : StateRecord> T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T
): T {
if (snapshot.readOnly) {
// If the snapshot is read-only, use the snapshot recordModified to report it.
snapshot.recordModified(state)
}
// ① ---------------------------------------
val id = snapshot.id
if (candidate.snapshotId == id) return candidate
// ② ---------------------------------------
val newData = newOverwritableRecord(state, snapshot)
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
这里面出现了新概念 Snapshot。之前说到,StateRecord 是一串,每个节点对应 Compose 不同状态下的值。一个变量对应一串 StateRecord。那么各串 StateRecord 哪些节点属于同一个状态呢?这就由 Snapshot 来记录。一个 StateRecord 只能对应一个 Snapshot,而一个 Snapshot 可以对应多个 StateRecord。多个 Snapshot 有先后关系,后者是建立在前者基础上的。举个例子:
如图,Record name 与 Snapshot1 对应。Snapshot2 基于 S1。虽然 S2 没有与 name 直接对应,但 S2 中如果取 name 的值,可以获取到 "bob",这是因为 name 的 Snapshot1 对 Snapshot2 是 有效的。
上面源码中 ① 部分就是判断,如果传入的 StateRecord 与 Snapshot 直接对应,那么就返回它。否则 ② 部分 就再搞一个 StateRecord 让它对应上,再返回。总是不管怎么样,必须返回一个对应上的就完事了。 具体的后面再研究。
搞清楚这个之后 overwritable() 第一部分就清晰了:把新值存到与当前 Snapshot 对应的 StateRecord 里。
also 块里顾名思义就是通知啦!它找到所有读取这个变量的地方,将它们标记为失效。那么下一帧这些地方就会重组 (recompose)。
订阅?通知?
看到这有同学可能发现了端倪。前面说 readable 是订阅,overwritable 是通知。但从代码看,两者都是对某个 Observer 调用了 invoke(),也就是说,这两个都是「通知」。WTF?
其实, snapshot.readObserver?.invoke(state) 既是通知,也是订阅。Compose 的订阅机制其实有两部分。
第一部分
第一部分是 Snapshot 中 StateObject 读和写事件的订阅。这两个事件的「通知」就是刚刚提到的两个 invoke 调用。而「订阅」是在 Snapshot 创建的时候发生的,本节没有分析到订阅的代码。
⚠️ 注意:writeObserver 只有在 compose 过程中发生修改,才会被通知,考虑下面的代码:
val name = mutableStateOf("Bob")
setContent {
Box(Modifier.clickable { name.value = "2" }) {
Text(name.value) // ①
name.value = "1" // ②
}
}
① 处发生了 name 的读事件,被记录。② 处发生了写事件,且处于 compose 过程中,此时 writeObserver 会被通知。而我们点击 Box 时,虽然也修改了 name,但这不是 compose 过程中,因此不会触发 write 通知。
虽然点击 Box 时不会通知,但是在「读」事件中已经订阅了「应用」事件(见下文)。UI 依然会刷新。
第二部分
第二部分是对每一个 StateObject「应用」事件的订阅。这个订阅是在「读」事件发生时进行的。所以 snapshot.readObserver?.invoke(state) 本身是通知,调用了「读」事件的观察者。而观察者内部所进行的操作,正是订阅「应用」事件。那么哪里通知呢?还没研究到嘿嘿。
说人话:我通知了你明天发年终奖,而你转手就打电话给苹果旗舰店,说 mbp 到货了立刻通知你。所以我的行为,既是一种通知,同时也引发了新的订阅。
所谓「应用」,就是让新值全局生效。可以类比成 sql 操作中事务的提交。
小结
SnapshotMutableStateImpl 中以链表形式储存了一串 StateRecord,每一个节点就是这个变量在某一状态时的值,与某个 Snapshot 对应。Snapshot 有先后关系,储存了某一状态下所有变量的值。
get 的过程中发生了「读」事件,监听器中又实现了对「应用」事件的订阅。
set 过程中发生了「写」事件,不过只有处于 compose 过程中这个事件才会真正触发,标记 UI 失效。
由于 Compose 存在两套订阅机制,因此既是不在 compose 过程中发生修改,UI 也会借助「应用」事件刷新。
by 关键字
上面已经解决掉了第一个问题,接下来看看把 = 换成 by 的作用是什么。
为了能自动刷新,我们把值进行了包装,这就导致每次读写时,不得不加上一个 .value 属性。同时哪怕是可以可变的值在声明时也可以写成 val 很有迷惑性。为解决这些问题,借助了 kotlin 原生的委托语法。
val test by Test() 意思就是 test 这个变量的读与写都由右边的对象来具体实现。为此,右边对象必须有两个固定名称与参数的函数,作为读、写的实现:
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
跟踪进源码,可以清晰地看到这俩的实现:
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
如此一来,我们可以像使用原生变量那样对包装的对象进行赋值,而一个可变的变量也必须声明为 var。
那么回答问题:如何选择使用?答:直接用 by 就行了!有简单的干嘛要找麻烦呢?
至于最后一种写法,下一节继续讨论。