前言
上章我们讲解了分包和自定义Composable,它的自动更新 UI 机制,本章我们讲解下 MutableState 和 mustableStateOf() 它的自动更新式的动态 UI 写法以及这个写法背后的自动订阅机制还有自动订阅机制背后可能影响我们开发的诡异东西;
mustableStateOf()
当我们在声明一个 Text 函数的时候,通过会传递一个 String 参数给这个 Text 函数,当这个 String 发生改变的时候,Text 自动更新成这个变化后的值,也就是声明式 UI 的本质
kotlin
val name = mutableStateOf("老A")
@Composable
fun ui() {
Text(text = name.value)
}
当我们给 name 赋值的是一个 mustableStateOf 包了一层的 "老A" 的时候,这个 name 持有的就不是一个字符串了,而是一个内部包含了 String 值的 MutableState 对象,好处就是这样的话它就可以被订阅了,坏处就是在使用它的时候不能直接传 name 而是要传 name.value 了;
到这时候可能就会有人有疑问了,为什么要这么写而不是直接传入 name,因为它内部的这个 value 的值才是我们真正要的值,才是对应的 "老A",那个可以真正改变的值已经不是 name 而不是 name.value 了,所以 name 的声明可以不用写成 var 而是可以写成 val 了,name 在这里已经变成对内部进行操作的操作元了,所以它就是不变,它内部也是可以改变的,不影响它内部属性的改变,所以 name 可以声明成 val 类型的,当这个 name.value 发生改成的时候,界面就可以直接更新这个 name.value 的最新值了,例如我们这样来实际看下效果:
kotlin
private var name = mutableStateOf("老A")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ui()
}
}
}
lifecycleScope.launch {
delay(3000)
name.value = "Mars"
}
}
@Composable
fun ui() {
Text(text = name.value)
}
启动一个协程,延迟 3s 更新 name.value 的值为 Mars,我们运行看下效果:
可以看到,3s 后更新成了 Mars; 那么问题来了,这到底是怎么做到的呢?这个自动订阅又是怎么订阅的呢?我们进入源码看下,完整的路径链路是
mutableStateOf -> createSnapshotMutableState -> ParcelableSnapshotMutableState -> SnapshotMutableStateImpl 我们进入这个 SnapshotMutableStateImpl 看下
可以看到我们所使用的 value 了,我们可以看到它的 get 和 set 函数都是有具体的实现的,简单来说当这个 value 被读的时候,会通过 next.readable 来记录下在哪被读了,同样的在它被写的时候,不仅仅是把这个值给修改了,还要找一找在哪里读过,然后去通知这些被读过的地方,让它们进行刷新;另外这个刷新是包含了三个部分的(组合、布局、绘制)这个组合在官方原表达中是 Composition,这个 Composition 就是 Compose 的意思,只不过一个是名词,一个是动词,也就是说 Compose 的实际过程就是先进行 Compose 再进行布局再进行绘制,这个跟我们所认知的传统界面绘制是不同的,传统界面绘制是只有布局和绘制,测量其实也是布局的过程,Compose 的布局过程也是分测量和布局的过程的;
组合
这个组合(Compose、Compostition)其实就是执行我们 Compose 代码的过程,就是执行那些被 Composable 注解标记的函数的过程,之所以单独搞这么一个过程,就是因为它是用来拼凑出实际的界面内容的过程;
那什么是拼凑出界面实际内容呢?Text("") 这不是界面实际内容吗?那么什么是界面实际内容呢?
按照 Compose 官方说法,它确实不是界面的实际元素,而是用于生成界面元素的,因为在布局和绘制的时候,Compose 并不是直接使用了我们的 Composable 函数来进行布局和绘制的,而是先用我们一个个的 Composable 函数来组合出一个个实际的对象,这些对象才是最终用来做布局和绘制工作的,
那么这个被组合被实际生成的对象又是什么呢? 我们在 setContent{} 中写的 Composable 函数,它们都会被装载进一个 ComposeView 这么一个类的对象里面,他里面又会包裹着一个 AndroidComposeView 的对象,这个 AndroidComposeView 对象中又会使用一个叫作 LayoutNode 类型的对象,用这个对象来进行真正的布局和绘制,而这些 LayoutNode 类型的对象就是在组合过程中生成的,Compose 通过组合过程拼凑出了 LayoutNode 这些对象,然后在后续的布局和绘制过程中利用这些 LayoutNode 去进行真正的布局和绘制;这个拼凑过程就是所谓的组合过程,也就是 Compose 过程;
value 的 get 具体实现
kotlin
get() = next.readable(this).value
private var next: StateStateRecord<T> = StateStateRecord(value)
这个 StateStateRecord 我们点击去看下:
kotlin
private class StateStateRecord<T>(myValue: T) : StateRecord() {
override fun assign(value: StateRecord) {
@Suppress("UNCHECKED_CAST")
this.value = (value as StateStateRecord<T>).value
}
override fun create(): StateRecord = StateStateRecord(value)
var value: T = myValue
}
继承自 StateRecord,我们这里标记下,下面会提到它
而 SnapshotMutableStateImpl 又是继承自 SnapshotMutableState
csharp
interface SnapshotMutableState<T> : MutableState<T> {
/**
* A policy to control how changes are handled in a mutable snapshot.
*/
val policy: SnapshotMutationPolicy<T>
}
SnapshotMutableState 又是继承自 MutableState,看起来是关联上来了,我们通过声明一个 mutableStateOf() 最终和 MutableState 关联起来,但是真正实现订阅能力的其实并不是这个,而是 StateObject,也就是 SnapshotMutableStateImpl 继承的另一个类
kotlin
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {}
我们进入这个 StateObject 看下
kotlin
interface StateObject {
val firstStateRecord: StateRecord
fun prependStateRecord(value: StateRecord)
fun mergeRecords(
previous: StateRecord,
current: StateRecord,
applied: StateRecord
): StateRecord? = null
}
实现比较简单,但这里有一个比较核心的点就是 StateRecord,而这个 StateRecord 就是我们前面 next 的类型 StateStateRecord 的父类,这个 StateRecord 才是真正存储状态订阅的类,也就是下面的关系图:
mutableStateOf -> createSnapshotMutableState -> ParcelableSnapshotMutableState -> SnapshotMutableStateImpl -> StateObject -> StateRecord
那么为什么要存到这个 StateRecord 中呢?而不是直接存到 StateObject 中呢?因为每个变量只存一份是不够的,旧值对 Compose 来说也是有用的,也是要存起来的,
为什么要存旧值呢?最主要的一个原因是 Compose 是支持事务功能的(可以批量进行,可以并发进行,可以撤销,可以事后进行合并的变量更新),如果更新可以撤销,那么旧值就需要存下来了,不然从哪里撤销呢?所以 Compose 的变量管理需要对变量存多个新旧值;
每个变量需要存多个新旧值,Compose 是怎么存的呢?Compose 采用的是链表的形式;链表这里简单说一下,它就是一个 List,但它的每一个元素不是放在一个数组中挨着排放的,而是依靠引用来连接的,它的每一个元素内部保存着对下一个元素的引用;
因为 Compose 采用的链表的形式,那么它只需要往 StateObject 的对象中保存一个 StateRecord 就可以了,就是我们看到的 firstStateRecord
csharp
override val firstStateRecord: StateRecord
get() = next
而 StateRecord 中有一个 next 属性
csharp
internal var next: StateRecord? = null
这个 next 属性是 StateRecord 类型的,通过这种方式,我们就可以访问到一个 StateObject 中的所有的 StateRecord 了,我们只需要获取到 firstStateRecord 之后,就可以获取所有的 StateRecord 了;
我们接着回去看 value:T 的 get 方法,next 也就清楚了,它就是链表的头节点,我们进入这个 next.readable 方法看下,最终调用到的是:
kotlin
fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T {
// invoke the observer associated with the current snapshot.
snapshot.readObserver?.invoke(state)
return readable(this, snapshot.id, snapshot.invalid) ?: readError()
}
snapshot.readObserver?.invoke(state) 这行代码就是进行记录的,它会记录SnapshotMutableStateImpl(StateObject) 对象被使用了,指的就是我们在执行 Text(text = name.value) 调用 name.value 的时候会被记录下来,这个记录它实际上是一个订阅类型的,因为这些 MustableState 对象每次被更新的时候,都会去遍历这些做过的记录,去看一看这个变量曾经都在哪些地方被读了,然后对这些读过的地方把它们标记为失效,这些失效的位置,在下一帧的时候这些失效的位置就会被刷新,更确切的说是会被重组,我们前面有说到 Compose 在绘制的时候分为三个部分:组合、布局、绘制,而这个重组,Compose 官方原表达是 Recompose,官方翻译过来就是重组,这个记录的动作,本质上就是订阅;
接着调用了三个参数的 readable 方法,这个三个参数的 readable 方法发生了什么?
主要是用来取值,取什么值呢?取得就是从链表表头的那个 StateRecord 开始遍历,去找到一个最新的、可用的 StateRecord,那这个最新的、可用的怎么理解呢?首先这个 StateRecord 是用来保存修改前和修改后的值的,这些值会随着状态的改变而失效,有的值在这个地方是有效的,在另一个地方就可能是无效的,所以针对有效性来说,我们不能只拿最新的,还得是有效的才行,所以针对这个三参数的 readable 方法,它的作用就是拿到最新的、可用的 StateRecord,然后把它给返回;
所以这个
csharp
override var value: T
get() = next.readable(this).value
做了两件事情:遍历这个 StateRecord 链表,拿到最新的可用的 StateRecord,顺便记录下这个 SnapshotMutableStateImpl(StateObject)它在这个地方被用到了;
.value 就是获取这个 StateRecord 中包裹的实际的值;
小结
为什么 mutableStateOf 这个返回的对象可以被订阅?
因为它的那个 value 属性的 get 函数被定制了,定制之后,它每次被取值的时候,都会先进行记录操作,记录下这个值在哪被取用了,然后再从保存的一大堆值中取出最新的、可用的那一个值然后返回
value 的 set 具体实现
kotlin
override var value: T
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
我们来看下这个 withCurrent 做了什么?
kotlin
inline fun <T : StateRecord, R> T.withCurrent(block: (r: T) -> R): R =
block(current(this))
实现很简单,就是直接调用了 block 函数,也就是直接调用了 withCurrent 后面跟的那个 lambda 函数
javascript
{ it:StateStateRecord<T>
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
可以看到这个 lambda 是有入参的,它是一个 StateStateRecord 类型的,也就是 current(this) 返回是一个 StateStateRecord 类型的,我们进入这个 current 看下
kotlin
internal fun <T : StateRecord> current(r: T) =
Snapshot.current.let { snapshot ->
readable(r, snapshot.id, snapshot.invalid) ?: sync {
Snapshot.current.let { syncSnapshot ->
readable(r, syncSnapshot.id, syncSnapshot.invalid)
}
} ?: readError()
}
可以看到,最终调用到了 readable 函数,我们前面讲了,这个 readable 就是取值的作用,所以 current 函数就是取一个最新的值作为参数传给 block 函数,我们来看看 block 做了什么?
javascript
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) {
this.value = value
}
}
比较一下新旧值是不是不一样,如果不一样,就进入 next.overwritable 函数,我们来看下这个函数
kotlin
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)
}
}
可以看到,它内部最终调用了一个 overwriteableRecord 函数,我们进入这个函数看下
kotlin
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 = sync { newOverwritableRecordLocked(state) }
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
这里面可以分为两部分来看,
一部分是:
bash
if (candidate.snapshotId == id) return candidate
这部分说的是:如果你传递过来的 StateRecord 的 snapshotId 正好等于传过来的 SnapShot,那么就直接把这个 StateRecord 返回;
一部分是:
ini
val newData = sync { newOverwritableRecordLocked(state) }
newData.snapshotId = id
snapshot.recordModified(state)
return newData
如果前面没有返回,就用 newOverwritableRecordLocked 获取一个 StateRecord 并返回;
Snapshot
这里额外多了一个 Snapshot,那么这个 Snapshot 是什么呢?
StateRecord 每次修改的新旧值都会被记录下来串成一个链表,这个链表上的各个节点其实都对应了某一个时刻的 Compose 的整个内部状态,Compose 记录每个变量的每个状态,用的是 StateRecord 的链表,而具体各个链表上的哪些节点它们共属于同一个状态,它也有记录,而这个记录就是 Snapshot;
StateRecord 对应的是变量;
Snapshot 对应的是整个状态,可以对应多个 StateRecord,一个 StateRecord 对应一个 Snapshot,这个 Snapshot 是对整个系统做快照的,有了这个快照之后,就可以在一些变量值发生变化的时候,不必把它马上应用到内部显示到界面,而是在它跑完整个 Compose 的流程之后,把所有改变的变量一起应用,然后拿着这个最终的结果,去进行接下来的布局和绘制,这样性能会好些,Snapshot 机制就对这种批量应用改变提供了下层技术可行性的支持;
所以,系统有多个 Snapshot 的时候,它们是有先后关系的;
同一个 StateObject 的每个 StateRecord,都有它们对应的 Snapshot 的 id,StateRecord 和 Snapshot 就算不直接对应,只要 StateRecord 的 Snapshot 对另一个是有效的,另一个就能取到这个 StateRecord;
newOverwritableRecordLocked 获取一个 StateRecord 并返回,它内部并不是直接创建一个新的,而是直接拿或者废物利用或者用新创建的方式来获取一个 StateRecord;
当我们通过
kotlin
this.writableRecord(state, snapshot).block()
拿到 StateRecord 之后,就会调用一个 block 函数,这个 block 函数对应的就是
ini
{ this.value = value }
很简单的一个赋值操作,把传入的新值赋值给内部的 value; 赋值完之后,还有一步操作:
kotlin
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
找到这个变量在哪里被读了,然后把这部分的 Compisition 组合结果标记为失效,然后等着到下一帧的时候,这些失效的部分,会重新 ReCompose 重组
到这里,value 的 get 和 set 就都分析完了,get 负责订阅,set 负责通知,每一次变量更新的时候就会应用到界面,就会更新了;
但是 这并不完全是 Compose 的自动订阅,因为 Compose 是有两套订阅系统的,它们共同工作才让变量真正被订阅了
我们回到代码中看下:
get 中的
arduino
snapshot.readObserver?.invoke(state)
set 中的
arduino
snapshot.writeObserver?.invoke(state)
它们都是 Snapshot 中的两个 Observer,而 Observer 就是订阅通知中的被通知对象,也就是说读,通知 readObserver,写通知 writeObserver,它们属于两个不同订阅中的两个被通知对象,readObserver 对读做了订阅,writeObserver 对写做了订阅,它们会分别收到读和写的通知;
到这的时候,可能会有人有疑问了,前面说 readObserver 是订阅,writeObserver 是通知,这里又说两个都是订阅,那么到底哪个是合理的?其实这两种都合理,因为它本来就有两套订阅,分别是对不同的对象做的订阅;
首先 Compose 要先去订阅 Snapshot,对它内部的读和写行为分别做订阅,对它们分别读写一个个的 StateObject 的行为做订阅,这样当我们的 Snapshot 去读和写任何一个 StateObject 对象的时候,我们的readObserver 和 writeObserver 就会收到相应的通知,通知某个对象被读或者被写了,这个对象就是 StateObject,所以我们其实是对 Snapshot 中的读和写进行订阅,并在读和写发生变化的时候进行通知,这两个订阅的通知部分就是上面的 readObserver.invoke 和 writeObserver.invoke,而它们的订阅行为是在 Snapshot 对象被创建的时候自动发生的;
这是 Compose 对 Snapshot 读、写 StateObject 行为的订阅,所以有两个接受者,readObserver 和 writeObserver;订阅发生在 Snapshot 创建的时候,通知发生了读和写的时候
另外 Compose 还会对具体每一个 StateObject 它的应用事件做订阅,订阅发生在它的第一个 readObserver 被调用(通知)的时候,通知发生在 StateObject 新值被应用的时候;
我们可以来看一个具体的例子
kotlin
private var text = mutableStateOf("1")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Ui()
}
}
}
}
fun Ui() {
Column(modifier = Modifier.fillMaxWidth().height(200.dp)
.border(2.dp, Color.Black)
.clickable {
text.value = "2"
}) {
Text(text = text.value)
text.value = "3"
}
}
我们前面有说到订阅发生在组合的时候,也就是
ini
{
Text(text = text.value)
text.value = "3"
}
这是组合的过程,当执行到 Text(text = text.value) 的时候,它的读被记录了,接下来又通过 text.value = "3" 触发了写,就会触发通知,通知这块区域将被标记为失效,如果没有 Text(text = text.value) 这一行将不会被标记为失效,因为 text.value 在任何地方都没有被读过,所以不会被标记为失效,只有被读过了,当执行到写的时候,才会被标记为失效,在下一帧的时候进行刷新;而 clickable 区域中发生点击的时候,text 的值会发生改变,但是这块区域不属于 Compose(组合)过程,不是在组合过程,发生的写事件不会被通知到 writeObserver,可能就会有人有疑问了,那界面不就不会更新了吗?不会的,我们还有一个应用事件,这个加起来才是一个完整的通知订阅机制;
mutableStateOf 的简化写法
使用 by 关键字
csharp
private var name by mutableStateOf("老A")
这样我们在调用 name.value 的时候,就可以直接省略成 name
ini
Text(text = name)
好了 MutableState 和 mutableStateOf 今天就讲到这里吧
下一章预告
重组作用域和 remember
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~