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 类似。所谓「组合」,就是执行 @Composable 函数,创建出实际的界面,组合的结果称为 Composition

插一句题外话,Compose 函数虽然看起来很像是创建一个对象,例如 Text(text = name.value) 很像创建一个 Text 对象,但实际上不是这样。跟踪源码可以发现,这只不过是一个普通的函数,它没有返回值,因此类似 Text().text = name.value 的写法完全错误 ❌
这与传统 View 中 TextView().text = "xxx" 不是一回事。

—— 所以需要「组合」

如果还是无法理解,可以将「组合」类比传统 View 中 inflat,@Composable 函数就像是 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 就是保存实际数据的东西,可惜它依然不是最终实现。继续探索便遇到一个关键类 StateRecordStateStateRecord 是它的子类),要搞清楚它,首先要看看 SnapshotMutableStateImpl 所实现的两个接口。现在我们已经知道 mutableStateOf() 可以返回一个容器对象,它能够被订阅并且在内部值改变时通知订阅者,以此实现 UI 自动刷新。「被订阅」这种特性其实是 StateObject 提供的,很反直觉。

再次套娃🪆,StateObject 本身又是一个容器,以链表的形式存储着一串 StateRecord ⬅️ 这个真正保存了数据。为什么是一串?因为它不仅保存了变量的最新值,还保留了曾经的值,以便实现「事务」特性。既然是链表,那肯定存在第一个节点,就是 StateObject.firstStateRecord ,后继节点就是 StateRecord.next。注意这个 next 和 SnapshotMutableStateImpl.next 无关,恰好重名而已。事实上 SnapshotMutableStateImpl 本身就有一个链表保存着所有值(它继承了 StateObject 呀),它的首个节点是 SnapshotMutableStateImpl.next

小结:StateObject 以链表形式真正储存了数据,StateRecord 是链表的节点,SnapshotMutableStateImpl 是一个 StateObject 的实现,它的 next 属性就是头节点。

get

get 操作调用 StateRecord.readable() 取得一个 StateRecord 进而取得其内部包装的实际值返回。

可能有小伙伴好奇,它自己不就是个 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 中如果取值依然可以获取到 "bob",这是因为 Snapshot1 对 Snapshot2 来说是有效的

上面源码中 ① 部分就是判断,如果传入的 StateRecord 与 Snapshot 直接对应,那么就返回它。否则 ② 部分 就再搞一个 StateRecord 让它对应上,再返回。总是不管怎么样,必须返回一个对应上的就完事了。 Snapshot 机制帮助实现了批量更新,即等所有变量都更新之后再一次性重组,也帮助处理了多线程问题,具体的后面再研究。

搞清楚这个之后 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 操作中事务的提交。

小结

mutableStateOf() 创建了一个对象,这个对象可以记录被使用的地方,并在内部属性更改时触发 UI 的刷新。

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 就行了! 有简单的干嘛要找麻烦呢?

至于最后一种写法 val name by remember { mutableStateOf("Bob") },下一节继续讨论。

Last modification:November 17, 2022