Compose快照系统(二)

265 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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。这是一个密封类,可以是SuccessFailure。如果我们在周围添加打印语句apply()调用,我们将看到第一个成功,但第二个失败。原因是更新之间存在无法解决的冲突——两个快照都试图根据相同的初始值(“Spot”)更改相同的名称值。因为第二个快照是以“Spot”的名称执行的,所以快照系统不能假设新的值“Fluffy”仍然是正确的。它要么需要在应用新快照后重新运行enter块,要么被明确告知如何合并名称。这与您尝试合并有冲突的 git 分支时的情况相同 - 合并将失败,直到您解决冲突。

但是Compose中有一个解决这种修改冲突的API.mutableStateOf()需要一个可选的SnapshotMutationPolicy. 该策略定义了如何比较特定类型的值 ( equivalent),以及如何解决冲突 ( merge)。一些政策开箱即用:

  • structuralEqualityPolicyequals– 通过使用对象的方法 ( )比较对象==,所有写入都被认为是非冲突的。
  • referentialEqualityPolicy– 通过引用比较对象 ( ===),所有写入都被认为是非冲突的。
  • neverEqualPolicy– 将所有对象视为不相等,所有写入都被认为是非冲突的。这应该被使用,例如,当快照包含一个可变值时,这用于指示该值以 == 或 === 无法检测到的方式更改。在可变状态对象中保存可变对象是不安全的(因为突变不是以任何方式隔离的),但如果对象实现不在您的控制范围内,则很有用。

Compose的重组原理

好了前面介绍完了Compose的快照系统的原理,那么接下来理解Compose的重组发生就会很容易,我们都知道Compose是通过状态的变化去驱动UI的更新,并且在Compose中重组是乐观的,智能的,它会确定重组最小范围进行重组,以减小性能开销。我们使用@Composable注解的函数都是带有RecomposeScope,scope内部会自动的跟踪访问状态,如果跟踪的状态发生变化那么就会进行重组,如没有变化就跳过重组。整个过程都是依靠前面我们了解的readObserverwriteObserver实现的

通过源码可以看到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,在下次帧信号到达时,标记为invalidatescope就会在重组中执行

 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中 ,如过在其他地方则不会触发重组