当前位置:   article > 正文

Jetpack Compose 深入探索系列五:State Snapshot System_snapshotstatelist

snapshotstatelist

Jetpack Compose 有一种特殊的方式来表示状态和传播状态变化,从而驱动最终的响应式体验:状态快照系统State snapshot system)。这种响应式模型使我们的代码更加强大和简洁,因为它允许组件根据它们的输入自动重组,并且只在必要时重组,避免了我们过去在Android View 系统中手动通知这些更改所需的所有样板文件。

什么是 Snapshot State

Snapshot state快照状态)是指可以被记录并观察其变化的隔离状态。当我们调用像mutableStateOf、mutableStateListOf、mutableStateMapOf、derivedStateOf、produceState、collectAsState等函数时,我们所得到的状态就是快照状态。所有这些函数都返回某种类型的状态,开发人员经常称其为快照状态。

Snapshot state” 这个术语的命名是因为它是 Jetpack Compose runtime 定义的状态快照系统的一部分。这个系统建模和协调状态变化和变化传播。它是以分离的方式编写的,因此理论上可以被其他想要依赖于可观察状态的库使用。

关于变化传播,我们在之前了解到的一件事情是所有 Composable 声明和表达式都会被 Jetpack Compose 编译器包装,以自动跟踪其体内的任何快照状态读取。这就是快照状态如何被(自动)观察的方式。目标是每当 Composable 读取的状态发生变化时,runtime 就会使其 RecomposeScope 失效,以便在下一次重组时再次执行它。

这是由 Compose 提供的基础设施代码,因此它不需要存在于任何客户端代码库中。runtime 的客户端,如 Compose UI,可以完全不需要了解失效和状态传播的方式,或者如何触发重组,而只需要关注提供与该状态配合使用的构建块:即 Composable 函数。

但是快照状态不仅仅是自动通知更改以触发重组的问题。使用 “snapshot” 这个单词的一个很重要的原因是:状态隔离。这代表了我们在并发上下文中应用的隔离级别。

想象一下在不同线程之间处理可变状态会怎样。这很容易变得一团糟。需要严格的协调和同步来确保状态的完整性,因为它可以在同时从不同的线程中读取或修改。这为冲突、难以检测的bug和竞争条件敞开了大门。

传统意义上,编程语言以不同的方式处理这个问题,其中之一是不可变性。不可变数据在创建后永远不会被修改,这使它在并发场景下绝对安全。另一个有效的方法可以是 actor 系统。该系统专注于跨线程的状态隔离。Actor 保留其自己的状态副本,通过消息实现通信/协调。如果该状态是可变的,则需要存在一些协调来使全局程序状态一致。Compose 快照系统不是基于 actor 系统,但实际上更接近于该方法。

Jetpack Compose 使用可变状态,因此 Composable 函数可以自动响应状态更新。仅使用不可变状态的库是没有意义的。这意味着它需要解决在并发场景中共享状态的问题,因为组合可以在多个线程中实现。Compose解决此问题的方法就是状态快照系统,它基于状态隔离和后续的变更传播,以便可以在多个线程之间安全地使用可变状态。

快照状态系统是使用并发控制系统建模的,因为它需要以安全的方式协调跨线程的状态。在并发环境中共享可变状态并不容易,这是一个通用的问题,与库的实际用例无关。

在 Jetpack Compose 中 State 是一个接口,任何快照状态对象都会实现这个接口。以下是State接口的代码形式:

在这里插入图片描述

这个协议被标记为 @Stable,因为 Jetpack Compose 仅提供和使用稳定的实现(出于设计原因),概括一下,这意味着该接口的任何实现必须确保:

  • 对相同的两个 State 实例调用 equals 方法总是返回相同的结果。
  • 当类型的公开属性值更改时,会通知组合。
  • 它所有的公开属性值类型也是稳定的。

建议读一下Zach Klipp的这篇文章,介绍了一些相关的想法。我非常推荐这篇文章。

接下来首先了解一些关于并发控制系统的知识。这将有助于我们更容易地理解为什么Jetpack Compose状态快照系统采用这种模型。

并发控制系统

状态快照系统是按照并发控制系统实现的,因此让我们先介绍这个概念。

在计算机科学中,“并发控制”是关于确保并发操作正确结果的一种方法,这意味着协调和同步。并发控制由一系列规则组成,确保整个系统的正确性。但是,协调总是伴随着一定的代价。协调通常会影响性能,因此关键挑战是设计一种尽可能高效但不会显著降低性能的方法。

一个并发控制的例子是数据库管理系统(DBMS)中的事务系统,这个上下文中的并发控制确保在并发环境中执行的任何数据库事务都是以安全的方式进行的,不违反数据库的数据完整性。目标是维护正确性。这里的“安全”涵盖的内容包括确保事务是原子性的、可以安全地撤销、已提交事务的效果永远不会丢失,以及已中止事务的效果不会留在数据库中。这是一个复杂的问题。

并发控制不仅在DBMS中经常出现,在编程语言中也会出现,例如用于实现事务内存。事务内存试图通过允许一组加载和存储操作以原子方式执行来简化并发编程。实际上,在Compose状态快照系统中,当状态更改从一个快照传播到其他快照时,状态写入被应用为单个原子操作。像这样分组的操作简化了并行系统/进程中共享数据的并发读写之间的协调。在此基础上,原子更改可以轻松地中止、撤销或重现。即:拥有可重现更改历史,以可能重新生成程序状态的任何版本。

并发控制系统有不同的类别:

  • 乐观:不阻塞任何读或写操作,并对这些操作的安全性持乐观态度,如果提交时将违反所需规则,则中止事务以防止违反。中止的事务立即重新执行,这意味着有开销。当平均中止事务的数量不太高时,这种策略可能是一个很好的选择。
  • 悲观:如果操作违反规则,则阻止事务中的操作,直到违反的可能性消失。
  • 半乐观:这是其他两种的混合解决方案。只在某些情况下阻止操作,并对其他情况持乐观态度(然后在提交时中止)。

每个类别的性能因因素而异,例如平均事务完成速率(吞吐量)、所需的并行性水平和其他因素,例如死锁的可能性。非乐观类

多版本并发控制 (MVCC)

Jetpack Compose 中的全局状态是跨组合和线程共享的。组合函数应该能够并行运行(可以随时进行并行重组),如果它们并行执行,则可以同时读取或修改快照状态,因此需要进行状态隔离。

并发控制的主要特性之一实际上是隔离性。该特性确保了在并发访问数据的情况下的正确性。实现隔离的最简单方法是阻止所有 readers 直到 writers 完成,但这会对性能产生极大的影响。MVCC(Multiversion concurrency control)可以做得更好。

为了实现隔离性,MVCC 保留了数据的多个副本(快照),因此每个线程都可以在给定时刻使用一个隔离的状态快照来工作。我们可以将它们理解为状态的不同版本(“多版本”)。线程所做的修改对其他线程来说是不可见的,直到所有本地更改完成并传播。

在并发控制系统中,这种技术被称为“快照隔离”,并且它被定义为用于确定每个“事务”看到哪个版本的隔离级别。

MVCC 还利用了不可变性,因此每当写入数据时,都会创建数据的新副本,而不是修改原始数据。这导致在内存中存储了相同数据的多个版本,就像对象的所有更改历史一样。在 Compose 中,这些称为“状态记录”。

MVCC 还具有的一个特点是它创建了时间点一致的视图。这通常是备份文件的一个特性,它表示给定备份上所有对象的引用保持一致。在 MVCC 中,通常通过事务 ID 来确保这一点,因此任何读操作都可以引用相应的 ID 来确定使用哪个版本的状态。这实际上是 Jetpack Compose 中的工作方式。每个快照都被分配了自己的 ID。快照 ID 是单调递增的值,因此它们自然地被排序。由于快照是通过它们的 ID 区分的,因此读取和写入是相互隔离的,无需进行锁定。

Snapshot

一个快照可以在任何时候被创建。它反映了程序在给定时刻(创建快照时)的当前状态(所有快照状态对象)。可以创建多个快照,它们都会获得自己独立的程序状态副本。也就是说,当前所有快照状态对象在那个时间点的状态副本(实现 State 接口的对象)的副本。

这种方法使得状态修改是安全的,因为在一个快照中更新一个状态对象不会影响到其他快照中同一个状态对象的副本。快照之间是隔离的。在有多个线程的并发场景中,每个线程都将指向不同的快照,因此指向不同的状态副本。

Jetpack Compose runtime 提供了 Snapshot 类来模拟程序的当前状态。任何代码只需要调用它的静态方法:val snapshot = Snapshot.takeSnapshot() 即可获取到一个快照。这将获取所有状态对象当前值的快照,并且这些值将被保留,直到 snapshot.dispose() 方法被调用。这将决定快照的生命周期。

快照有其生命周期。每当我们使用完一个快照时,它都需要被处理掉。如果我们不调用 snapshot.dispose() ,我们将泄漏所有与该快照相关的资源及其保留状态。快照在创建释放状态之间被视为处于活动状态

当一个快照被创建时,它被赋予一个 ID,以便所有在该快照上的状态可以轻松地与其他潜在版本的相同状态区分开来。这允许为程序状态进行版本控制,或者换句话说,根据版本(多版本并发控制)使程序状态保持一致

最好的理解快照是通过代码。我将直接从Zach Klipp的这篇非常值得学习且详细的帖子中提取一段代码来说明:

在这里插入图片描述
其中 Dog 类的 name 是一个 mutableStateOf("") 的实现。

这里函数 snapshot.enter,通常称为“进入快照”,这会在快照的上下文中运行一个 lambda 表达式,因此快照成为任何状态的真实来源:从 lambda 表达式读取的所有状态将从快照中获取其值。这个机制允许 Compose 和任何其他客户端库在给定快照的上下文中运行任何处理状态的逻辑。这个过程在本地线程中进行,直到调用 enter 返回。其他任何线程都不会受到任何影响。

在上面的例子中,我们可以看到更新后的狗名为“Fido”,但是如果我们从快照的上下文(enter调用)读取它,它会返回“Spot”,这是在快照被创建时它所拥有的值。

当然,在使用完快照后必须记得调用 snapshot.dispose() 来释放状态,下面是完整代码:

class Dog {
   
    var name: MutableState<String> = mutableStateOf("")
}

fun main() {
   
    val dog = Dog()
    dog.name.value = "Spot"
    val snapshot = Snapshot.takeSnapshot()
    dog.na
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/254172
推荐阅读
相关标签
  

闽ICP备14008679号