持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
Compose快照系统(二)
接着前天我们分享的Compose快照系统继续分析Compose是怎么进行重组,又是怎么确定重组的最小范围的呢?
多线程
在给定的线程的快照中,在应用快照之前,不会看到其他线程对该状态值做的修改。如果代码需要对某个状态进行操作,并且希望其他线程不可以干扰它,那么通常该代码会使用互斥锁之类的东西来保证快照的互斥访问。但是,快照是具有隔离性的,所以它可以只取一个快照,然后对快照内部的状态的内部进行操作。如果其他线程修改了状态,那么在应用快照之前,拥有快照的线程是不会知道这些修改的。所以快照内状态所做的修改是对其他线程都是不可见的。
快照的冲突写入
前面的例子中我们都只是拍摄了一个快照,那么我们拍摄了多个快照然后在同时对状态进行修改会是怎样呢?接下来我们就看下结果
@Composable
fun HomeScreen(){
val viewModel: HomeViewModel = viewModel()
viewModel.name.value = "Compose"
val snapshot1 = Snapshot.takeMutableSnapshot()
val snapshot2 = Snapshot.takeMutableSnapshot()
snapshot1.enter{
viewModel.name.value = "SnapShot1"
Log.d("SnapShot",viewModel.name.value)
}
snapshot2.enter{
viewModel.name.value = "SnapShot2"
Log.d("SnapShot",viewModel.name.value)
}
snapshot1.apply()
snapshot2.apply()
}
我们拍摄了两个快照,并且在不同快照的enter中对状态进行了更改,通过日志输出可以看出第一个快照成功的应用了他的修改,但是第二个快照,并没修改成功,要了解这里发生了什么,让我们仔细看看这个apply()方法。它实际上返回一个 type 的值SnapshotApplyResult。这是一个密封类,可以是Success或Failure。如果我们在周围添加打印语句apply()调用,我们将看到第一个成功,但第二个失败。原因是更新之间存在无法解决的冲突——两个快照都试图根据相同的初始值(“Spot”)更改相同的名称值。因为第二个快照是以“Spot”的名称执行的,所以快照系统不能假设新的值“Fluffy”仍然是正确的。它要么需要在应用新快照后重新运行enter块,要么被明确告知如何合并名称。这与您尝试合并有冲突的 git 分支时的情况相同 - 合并将失败,直到您解决冲突。
但是Compose中有一个解决这种修改冲突的API.mutableStateOf()需要一个可选的SnapshotMutationPolicy. 该策略定义了如何比较特定类型的值 ( equivalent),以及如何解决冲突 ( merge)。一些政策开箱即用:
structuralEqualityPolicyequals– 通过使用对象的方法 ( )比较对象==,所有写入都被认为是非冲突的。referentialEqualityPolicy– 通过引用比较对象 (===),所有写入都被认为是非冲突的。neverEqualPolicy– 将所有对象视为不相等,所有写入都被认为是非冲突的。这应该被使用,例如,当快照包含一个可变值时,这用于指示该值以 == 或 === 无法检测到的方式更改。在可变状态对象中保存可变对象是不安全的(因为突变不是以任何方式隔离的),但如果对象实现不在您的控制范围内,则很有用。
Compose的重组原理
好了前面介绍完了Compose的快照系统的原理,那么接下来理解Compose的重组发生就会很容易,我们都知道Compose是通过状态的变化去驱动UI的更新,并且在Compose中重组是乐观的,智能的,它会确定重组最小范围进行重组,以减小性能开销。我们使用@Composable注解的函数都是带有RecomposeScope,scope内部会自动的跟踪访问状态,如果跟踪的状态发生变化那么就会进行重组,如没有变化就跳过重组。整个过程都是依靠前面我们了解的readObserver和writeObserver实现的
通过源码可以看到Compose在执行时注册了这两个观察者
private inline fun <T> composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
//创建快照
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
// 进入快照
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot)
}
}
readObserverOf记录了那些RecomposeScope对其状态进行了访问
override fun recordReadOf(value: Any) {
if (!areChildrenComposing) {
composer.currentRecomposeScope?.let {
it.used = true
observations.add(value, it)
...
}
}
}
writeObserverOf这个方法时确定状态在写入时哪些RecomposeScope受影响,并且将其标记invalidate,在下次帧信号到达时,标记为invalidate的scope就会在重组中执行
override fun recordWriteOf(value: Any) = synchronized(lock) {
invalidateScopeOfLocked(value)
derivedStates.forEachScopeOf(value) {
invalidateScopeOfLocked(it)
}
}
private fun invalidateScopeOfLocked(value: Any) {
observations.forEachScopeOf(value) { scope ->
if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) {
observationsProcessed.add(value, scope)
}
}
}
注意:重组一定发生在ReComposeScope中 ,如过在其他地方则不会触发重组