深入浅出 JetPack Compose UI 自动更新原理

news2024/10/1 7:33:07

为什么需要学习 Compose 的底层原理?

我想学习底层原理的主要原因是我对 Jetpack Compose 是如何运作的非常感兴趣。相对于传统的 View 系统,JetPack Compose 写起来相当神奇。具体来说,Composable 函数不会有返回值;它会自动更新。

当然,考虑到它会是未来 Android UI 开发主流框架,我相信探究它的底层原理绝不是在浪费时间。并且如后面正文描述那样,我发现 Jetpack Compose 的实现非常有意思以至于我忍不住想要与你们分享。

我们如何学习?

我会展示一个简单的 Compose 代码片段并解释它如何工作来加深你对其中原理的认知。

这篇文章的使用例子是一段简单的 Compose UI 树代码,如下所示

@Composable
fun Content() {
    // 3秒后 state 变为 false
    var state by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(3000)
        state = false
    }
    //当 state 由 true 变为 false 时,Node1()会消失
    if (state) {
        Node1()
    }
    Node2()
}

所有的代码都在这个仓库里,如果你看不懂文章下面的介绍,你也可以选择直接看代码,只有130行,应该不会很难。

UI 树如下所示,并且 Node1 将会3秒后消失。

RootNode
├── Node1(value=node1)
└── Node2(value=node2)

3秒后UI树变成这样↓

RootNode
└── Node2(value=node2)

让我们看看 Compose 是如何实现上面的功能的。

首先,我们需要构造一棵上述的树,我们先简单地定义一个 Node 类。这里,我们定义了 Node 类,和它的三个子类: RootNode, Node1, Node2. 上面的几个类不会继承任何 Compose 框架里的类。

sealed class Node {
    val children = mutableListOf<Node>()

    class RootNode : Node() {
        override fun toString(): String {
            return rootNodeToString()
        }
    }

    data class Node1(
        var name: String = "",
    ) : Node()

    data class Node2(
        var name: String = "",
    ) : Node()
}

我们要如何让 Compose 使用上面的类来构造我们想要的树呢?

Compose 框架里有个 Applier 类,就是用来处理 UI 树的,其中的 UI 节点增删改查,都是由它来进行。

class NodeApplier(node: Node) : AbstractApplier<Node>(node) {
...
    override fun insertTopDown(index: Int, instance: Node) {
        // Now add a child node!
        current.children.add(index, instance)
    }

    override fun move(from: Int, to: Int, count: Int) {
        current.children.move(from, to, count)
    }

    override fun remove(index: Int, count: Int) {
        current.children.remove(index, count)
    }
}

为了让 Applier 添加我们自定义的 Node 类,我们要让 Compose 去管理 Node.

通过使用 ReusableComposeNode, 我们可以在 Compose 内部里添加 Node 类。在构造 ReusableComposeNode 的时候,需要我们传入两个 lmbda 参数,我们由参数名就可以理解到它们各自的作用:其中 factory 用于构造自定义类, update 用于当参数改变的时候发生的回调。

@Composable
private fun Node1(name: String = "node1") {
    ReusableComposeNode<Node.Node1, NodeApplier>(
        factory = {
            Node.Node1()
        },
        update = {
            set(name) { this.name = it }
        },
    )
}

@Composable
private fun Node2(name: String = "node2") {
    ReusableComposeNode<Node.Node2, NodeApplier>(
        factory = {
            Node.Node2()
        },
        update = {
            set(name) { this.name = it }
        },
    )
}

有了上面的代码我们就可以让 Compose 帮我们构造出上面的自定义 Node 类了,你可能需要等其他的解释完了才能理解这部分代码。

fun runApp() {
    val composer = Recomposer(Dispatchers.Main)

    GlobalSnapshotManager.ensureStarted()
    val mainScope = MainScope()
    mainScope.launch(DefaultChoreographerFrameClock) {
        composer.runRecomposeAndApplyChanges()
    }

    val rootNode = Node.RootNode()
    Composition(NodeApplier(rootNode), composer).apply {
        setContent {
            Content()
        }
    }
}

这篇文章的总览图

图片实在太大了,直接贴上来被压得妈都不认得,原图在地址在这里,个人觉得可以先阅读完整篇文章后再看图来作为回顾:github.com/takahirom/i…

之前提及的示例代码在运行前后会发生一些事情,梳理出这些应该可以帮助你理解 Compose 的内部机制:

在编译时

  1. 为了创建 SlotTable, Compose 的 Kotlin 编译插件会改写 Composable 函数

在运行时

  1. 调用 Composable 函数并在 SlotTable 中储存一些信息

  2. 3秒后改变 MutableState

  3. Snapshot 系统监听到改变

  4. 重组

  5. 将发生的改变映射到 SlotTable(内部改变时使用 GapBuffer 算法)

下面我们一步一步细说

第0步:为了创建 SlotTable, Compose 的 Kotlin 编译插件会改写 Composable 函数

Compose 既然能在参数改变的时候进行更新,那它一定是储存了一些信息用于判断 Composable 参数是否改变了。Compose 框架中的 SlotTable(后面会细说) 正是担任这样的角色,这些参数信息是随着 Composable 函数储存在 SlotTable 中的。

我们的代码在编译成 Android 运行的应用时,Kotlin 代码会被转化为 Java 字节码,然后这些 Java 字节码再被转化为虚拟机字节码。而 Compose 看起来重写了 Kotlin IR 编译器。

因此我们可以把我们的 Kotlin 代码编译出来的字节码反编译,我们就能知道 Compose 的编译器插件做了什么事情。

现在就让我们看看反编译后的 Content() 函数。

  • 你会注意到代码中多了 startRestartGroup(), endRestartGroup(), startReplaceableGroup(), endReplaceableGroup() 这几个不是我们写的方法。 Compose 框架内部有组的概念,并且允许我们使用组来创建 UI 树。

  • 把成组的代码以某种方式保存使得可以在需要的时候被重复执行

  • 你也可以看到代码里还有看起来像是跳过执行的方法

   @Composable
   public static final void Content(@Nullable Composer $composer, final int $changed) {
      // ↓↓↓↓RestartGroup↓↓↓↓ 
      $composer = $composer.startRestartGroup(-337788314);
      ComposerKt.sourceInformation($composer, "C(Content)");
      if ($changed == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
... LaunchedEffect and MutableState related code
         $composer.startReplaceableGroup(-337788167);
         if (Content$lambda-2(state$delegate)) {
            Node1((String)null, $composer, 0, 1);
         }

         $composer.endReplaceableGroup();
         Node2((String)null, $composer, 0, 1);
      }

      ScopeUpdateScope var18 = $composer.endRestartGroup();
      // ↑↑↑↑RestartGroup↑↑↑↑
      // ↓↓↓↓保存上面的组代码使得可以在被需要的时候重复执行↓↓↓↓ 
      if (var18 != null) {
         var18.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Content($composer, $changed | 1);
            }
         }));
      }
      // ↑↑↑↑保存上面的组代码使得可以在被需要的时候重复执行↑↑↑↑

   }


   @Composable
   private static final void Node1(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931657);
// ...
      ScopeUpdateScope var10 = $composer.endRestartGroup();
      if (var10 != null) {
         var10.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Node1(name, $composer, $changed | 1, var3);
            }
         }));
      }

   }

第1步:调用 Composable 函数并且把关键信息储存至 SlotTable

现在,运行APP,示例代码中的 Content() 函数会被执行。

在这个时候, Compose 会把数据储存至 SlotTable. 在这篇文章里我们不会花太多篇幅在这步上,因为我们主要关心 UI 自动更新的部分。

关于 SlotTable

SlotTable 内部包含两个数据结构,都是用于储存 UI 树中的组信息。

groups: IntArray
slots: Array<Any?>

这个名为 groups 的整型数组为每个组储存了5个元素,因此 groups 的大小是组数量的5倍。有了这个信息后我们也可以通过下面的代码来窥探 groups 里储存的数据:

groups.toList().windowed(
    size = 5,
    step = 5,
    partialWindows = false
)
    .forEachIndexed { index, group ->
        val (key, groupInfo, parentAnchor, size, dataAnchor) = group
        println("index: $index, " +
                "key: $key, " +
                "groupInfo: $groupInfo, " +
                "parentAnchor: $parentAnchor, " +
                "size: $size, " +
                "dataAnchor: $dataAnchor")
    }

看起来 groups 储存的数据是属于 slots 的信息。

组数据里储存着对应 slots 的下标,在上面名为 dataAnchor 的变量中。组数据里也储存着对应父组的 slots 的下标,在上面名为 parentAnchor 的变量中。

groups 中的信息:

index: 0, key: 100, groupInfo: 2, parentAnchor: -1, size: 16, dataAnchor: 0
index: 1, key: 1000, groupInfo: 2, parentAnchor: 0, size: 15, dataAnchor: 1
index: 2, key: 200, groupInfo: 536870914, parentAnchor: 1, size: 14, dataAnchor: 1
index: 3, key: -985533309, groupInfo: 2, parentAnchor: 2, size: 13, dataAnchor: 2
index: 4, key: -337788314, groupInfo: 268435458, parentAnchor: 3, size: 12, dataAnchor: 4
index: 5, key: -3687241, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 6
index: 6, key: -3686930, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 8
index: 7, key: 1036442245, groupInfo: 268435456, parentAnchor: 4, size: 2, dataAnchor: 11
index: 8, key: -3686930, groupInfo: 268435456, parentAnchor: 7, size: 1, dataAnchor: 12
index: 9, key: -337788167, groupInfo: 1, parentAnchor: 4, size: 4, dataAnchor: 15
index: 10, key: 1815931657, groupInfo: 1, parentAnchor: 9, size: 3, dataAnchor: 15
index: 11, key: 1546164276, groupInfo: 268435457, parentAnchor: 10, size: 2, dataAnchor: 16
index: 12, key: 125, groupInfo: 1073741824, parentAnchor: 11, size: 1, dataAnchor: 17
index: 13, key: 1815931930, groupInfo: 1, parentAnchor: 4, size: 3, dataAnchor: 19
index: 14, key: 1546164276, groupInfo: 268435457, parentAnchor: 13, size: 2, dataAnchor: 20
index: 15, key: 125, groupInfo: 1073741824, parentAnchor: 14, size: 1, dataAnchor: 21
index: 16, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 17, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 18, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 19, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 20, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 21, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 22, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 23, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 24, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 25, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 26, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 27, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 28, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 29, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 30, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0
index: 31, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0

Slots 中的信息:

0: {}(class androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap)
1: OpaqueKey(key=provider)(class androidx.compose.runtime.OpaqueKey)
2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6(class androidx.compose.runtime.RecomposeScopeImpl)
3: androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827(class androidx.compose.runtime.internal.ComposableLambdaImpl)
4: C(Content)(class java.lang.String)
5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4(class androidx.compose.runtime.RecomposeScopeImpl)
6: C(remember):Composables.kt#9igjgp(class java.lang.String)
7: MutableState(value=true)@167707773(class androidx.compose.runtime.ParcelableSnapshotMutableState)
8: C(remember)P(1):Composables.kt#9igjgp(class java.lang.String)
9: MutableState(value=true)@167707773(class androidx.compose.runtime.ParcelableSnapshotMutableState)
10: Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>(class com.github.takahirom.compose.MainKt$Content$1$1)
11: C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp(class java.lang.String)
12: C(remember)P(1):Composables.kt#9igjgp(class java.lang.String)
13: kotlin.Unit(class kotlin.Unit)
14: androidx.compose.runtime.LaunchedEffectImpl@8d3f428(class androidx.compose.runtime.LaunchedEffectImpl)
15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3(class androidx.compose.runtime.RecomposeScopeImpl)
16: C(ReusableComposeNode):Composables.kt#9igjgp(class java.lang.String)
17: Node1(name=node1)(class com.github.takahirom.compose.Node$Node1)
18: node1(class java.lang.String)
19: androidx.compose.runtime.RecomposeScopeImpl@81cf51f(class androidx.compose.runtime.RecomposeScopeImpl)
20: C(ReusableComposeNode):Composables.kt#9igjgp(class java.lang.String)
21: Node2(name=node2)(class com.github.takahirom.compose.Node$Node2)
22: node2(class java.lang.String)
23: null(null)
24: null(null)
25: null(null)
26: null(null)
27: null(null)
28: null(null)
29: null(null)
30: null(null)
31: null(null)

另外,我们可以通过调用 SlotTable#asString() 来把数据可视化:

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827]
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@167707773]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@167707773, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@8d3f428]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@81cf51f]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

在运行时上面这些数据都会储存在 SlotTable 中

第2、3步:3秒后改变 MutableState, 快照系统监测到改变发生

下面这段代码改变 MutableState

// It's written at the top level. (The exact scope is different).
var state: MutableState<Boolean> = mutableStateOf(true)

@Composable
fun Content() {
    LaunchedEffect(Unit) {
        delay(3000)
        // Rewrite state.value
        state.value = false
    }

现在让我们探究一下 Compose 框架是如何监测到 State 的改变的

我们要如何监测 UI 里的数据改动?

Compose 框架使用快照系统去做这个事情,假设我们有如下代码,我们可以先猜猜代码会输出什么

class ViewModel {
    val state = mutableStateOf("initialized")
}

fun main() {
    val viewModel = ViewModel()
    Snapshot.registerApplyObserver { changedSet, snapshot ->
        changedSet.forEach {
            println("registerApplyObserver:" + it)
        }
    }
    viewModel.state.value = "one"
}

正确答案是什么都不会输出。

但是当我再增加一行 Snapshot.sendApplyNotifications() 结果就会不一样:

class ViewModel {
    val state = mutableStateOf("initialized")
}

fun main() {
    val viewModel = ViewModel()
    Snapshot.registerApplyObserver { changedSet, snapshot ->
        changedSet.forEach {
            println("registerApplyObserver:" + it)
        }
    }
    viewModel.state.value = "one"
    // ↓ **Add the following**
    Snapshot.sendApplyNotifications()
}

//控制台输出
//registerApplyObserver:MutableState(value=one)@1831932724

Compose 框架正是使用上面的机制来以监测每帧之间的 UI 数据变化。在上面的例子中,在 registerApplyObserver() ****注册的回调会在 state 改变后调用

如果 Compose 的重组发生在非主线程会发生什么事情呢?

从我们以前的经验来说,感觉这样做会出现异常。但是通过使用快照系统,没问题!

Compose 是如何妥善处理 state 被多个线程更改的?

事实上,在上一个例子里,我们使用了一个最顶层的快照 (GlobalSnapshot) 。通过调用 Snapshot.takemutableSnapshot() 就可以创建出快照,重组就是在快照里面发生。

    // Here is GlobalSnapshot

    // Create Snapshot
    val snapshot = Snapshot.takeMutableSnapshot()
    snapshot.enter {
         // Recompose here
    }

你可以把快照想象成游戏里的存档。

在创建快照后,通过 enter{} 方法进入快照,此时数据会定格在进入快照的时候,之后无论外面的数据怎么变化,都不会影响到该快照里面的数据。

class ViewModel {
    val state = mutableStateOf("init")
}

fun main() {
    val viewModel = ViewModel()

    viewModel.state.value = "before snapshot"

    val snapshot = Snapshot.takeMutableSnapshot()
    // 尝试从其他线程更改数据
    thread { // 启动一个其他线程更改 state
        viewModel.state.value = "changes from other thread"
    }
    snapshot.enter {
        // 等待上面的线程更改数据
        Thread.sleep(100)
        println("in snapshot:" + viewModel.state)
    }
}

//控制台的输出:
//in snapshot:MutableState(value=before snapshot)@1777466639

从上面的代码结果可以看出,在进入快照后,数据之后在外面发生的更改不会影响到快照内的数据。

那么当我们同时在快照里和快照外更改数据后会发生什么呢?

通过调用 Snapshot.apply() 我们可以把快照里的更改提交。

    val snapshot = Snapshot.takeMutableSnapshot()
    thread {
        viewModel.state.value = "changes from other thread"
    }
    snapshot.enter {
        Thread.sleep(100)
        println("in snapshot before change:" + viewModel.state)
        viewModel.state.value = "change in snapshot"
        println("in snapshot after change:" + viewModel.state)
    }
    snapshot.apply()
    println("after apply:" + viewModel.state)
    
//控制台的输出:
// in snapshot before change:MutableState(value=before snapshot)@1170114219
// in snapshot after change:MutableState(value=change in snapshot)@1170114219
// after apply:MutableState(value=changes from other thread)@1170114219

运行没有出错,但是看起来快照的更改没有生效。

事实上,MutableState 还有一个名为 SnapshotMutationPolicy 的参数(如下所示),用于快照提交时如果发生冲突时回调,让你自定义逻辑,而默认实现是会放弃提交的。快照系统正是通过这个来应对多线程修改。

fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)


class ViewModel {
    val state = mutableStateOf("init", object : SnapshotMutationPolicy<String> {
        override fun equivalent(a: String, b: String): Boolean {
            return a == b
        }

        override fun merge(previous: String, current: String, applied: String): String {
            return applied
        }
    })
}

Compose 是如何找到使用了更改的 state 的 Composable 函数的?

当一个 Composable 函数使用到的 MutableState 发生改变的时候,函数会再次调用,这个过程也叫重组 (Recompose) 。

这是如何做到的?

锵锵!Snapshot.takeMutableSnapshot() 方法还有一个名为 readObserver 的参数,问题的答案就在其中:

    val snapshot = Snapshot.takeMutableSnapshot(readObserver = { state ->
        // state 被读取的时候发生回调
    })

非常直观,当 state 被读取的时候,把当前的 Scope 存下来,如下所示。

class ViewModel {
    val state = mutableStateOf("init")
}

fun main() {
    val viewModel = ViewModel()
    lateinit var currentScope: String
    val observations = mutableMapOf<Any, String>()
    val snapshot = Snapshot.takeMutableSnapshot(readObserver = {
        observations[it] = currentScope
    })
    snapshot.enter {
        currentScope = "Root()"

        currentScope = "Content()"
        // read
        viewModel.state.value
        currentScope = "Root()"
    }
    snapshot.apply()
    observations.forEach { mutableState, scope ->
        println("`$mutableState` is observed by `$scope`")
    }
}

//控制台输出:
//`MutableState(value=init)@1096979270` is observed by `Content()`

还记得我们一开始的那个简易 UI 树例子的反编译后的代码吗? Compose 的编译器插件为我们生成了一个再次调用该 Composable 的方法,事实上,重组时的 UI 更新就是通过调用这个方法做到的:

   @Composable
   public static final void Content(@Nullable Composer $composer, final int $changed) {
...
      // ↓↓↓↓Register the function to be called again↓↓↓↓ 
      if (var18 != null) {
         var18.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Content($composer, $changed | 1);
            }
         }));
      }
      // ↑↑↑↑Register the function to be called again↑↑↑↑

   }

至此,我们知道了 :

  • Compose 如何监测数据发生改变

  • 数据改变后哪些关联的 Scope 被影响到

  • 被影响到的 Scope 如何更新UI

相信你已经可以推理出完整的过程了!

快照究竟是什么?

引用下wiki:

多版本并发控制(Multiversion concurrency control, MCCMVCC),是 数据库管理系统 常用的一种 并发控制 ,也用于程序设计语言实现 事务内存 。([1])

MVCC意图解决 读写锁 造成的多个、长时间的读操作饿死写操作问题。每个事务读到的数据项都是一个历史 快照 使得事务看到它启动时的数据状态。

第四步:重组 (Recompose)

当快照检测到 Content() 里读取的数据发生改变,Content() 会被再次调用。对于这个具体例子来说, state 被储存在 SlotTable 中,现在 Node1() 将要消失,而 Node2() 依旧会显示。

@Composable
fun Content() {
    var state by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(3000)
        state = false
    }
    if (state) {
        Node1() // ← This place is about to disappear.
    }
    Node2()
}

只重调发生改变的 Composable 函数(donut-hole skipping [我想翻译为部分调用,但是感觉不太准确,还是把原文的叫法贴出来了])

但是 Compose 会调用整个受影响的地方吗? Compose 其实做了很多精妙的优化。

Compose 会跳过数据没有变化的部分。这个优化被称为 ‘donut-hole skipping’ ,具体来说,Content() 会被调用,但不会完全调用(笑),里面的 Node2() 不会发生重组,就像把 Content() 比作一个甜甜圈(donut),而 Node2() 正是甜甜圈里的洞 (hole)。

非常神奇吧!在上文中我们提及过数据会被储存在 SlotTable 中,通过比对新旧数据,是可以做到跳过没有发生更改的部分的。

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
...
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@81cf51f]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       // **↓I won't recompose it because it's the same data I'm holding here!**
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

通过反编译的代码👇,我们可以看到第9行从 SlotTable 中读取数据比较。(事实上这里的默认参数并不准确,不过这个不是重点,我们这里不展开)

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931962);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
         $dirty = $changed | 6;
      } else if (($changed & 14) == 0) {
         // ↓ 与 SlotTable 中的数据比较
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         // 根据比较结果决定是否跳过
         $composer.skipToGroupEnd();
      } else {
...
         $composer.startReplaceableGroup(1546164276);
         // ... Here's the original process.
         $composer.endReplaceableGroup();
      }

...
   }

如何把改变映射到 SlotTable 中?

上一小节我们谈及到了未更改的部分不会重新运行,但是 SlotTable 的数据也是需要更新的。在重组期间,所有的改变都会被储存在一个 change list 中,最后一次过对 SlotTable 应用所有的改变。

第五步:使用 GapBuffer 算法优化 SlotTable 内部数据改动效率

现在是时候把更改列表储存的所有更改全部应用了。如果我们不想太多直接用常规方法实现,工作起来如下图所示,很直观,没有毛病。

那如果用上面的方法在4个元素的数组里移除3个呢?

由于删除数组中的其中一个数据后需要把后面所有的元素一个一个往回补,这样的操作会花费大量的时间👇

在前文我们已知 SlotTable 使用的数据结构就是数组,更何况现实中的 UI 元素可远不止个位数,显然不能用这种默认实现去操作 SlotTable。

Compose 使用 Gap Buffer 算法来优化。

简单来说,gap buffer 是一个动态数组,实现了相邻位置元素的高效的增删,这个算法常用于文本编辑器中。

通过移动数据来创建间隙,只需使用一个变量来表示间隙的长度,然后更改该变量即可完成删除。

使用 GapBuffer 删除 Node1 的过程👇

总结

可以看到,Compose 内部做了非常多有趣的优化,我不认为只使用 Compose 去编写 UI 能够发掘出这些东西。

  • Compose 的编译器插件会对我们写的 Composable 函数作处理,生成 SlotTable 所需要的组信息以及用于重组时调用的 Lambda
  • SlotTable 会在应用运行后创建
  • Composable 监听使用到的 State 发生改变是由快照系统实现的
  • 重组发生时会从 SlotTable中取出旧数据对比是否发生改变,以跳过不必的重组
  • 发生改变时不会马上触发重组,而是把信息储存在一个容器中,周期性(帧间)地触发所有容器中相关的重组
  • SlotTable 中的数据更新使用 Gap Buffer 算法优化

Android 学习笔录

Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1180662.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NVIDIA Jetson SOC 内存分配策略

CPU 是Host, GPU 是Device, 系统内存分配策略如下: 这段话的翻译如下&#xff1a; 集成的GPU会和CPU以及其他Tegra引擎共享DRAM&#xff08;动态随机存储器&#xff09;&#xff0c;并且CPU可以通过将DRAM的内容移动到交换区域&#xff08;SWAP area&#xff09;或者相反来控制…

每个 .vue 文件最多可以包含一个顶层 <template> 块

问题: 回答: 实际解决方法: 上图将template的#header写到了上一行的el-table-column中就出发这个问题

直播带货小程序的前端开发技巧与工具

当下&#xff0c;直播带货小程序已经成为了一个备受欢迎的选择&#xff0c;因为它能够将传统直播和电子商务相结合&#xff0c;为用户提供更好的购物体验。本文将深入探讨直播带货小程序的前端开发技巧和工具&#xff0c;以帮助开发人员更好地构建这一类型的应用。 1.小程序基础…

【Orangepi Zero2 全志H616】驱动OLED屏应用-IIC协议、设备的映射(mmap)

一、OLED屏幕 二、Orangepi的IIC接口 三、wiringPi库示例代码 四、Source insight 初步分析wiringP源码OLED_DEMO 五、设备的映射 六、mmap函数 七、简单OLED屏幕开发及实现 一、OLED屏幕 二、Orangepi的IIC接口 1) 由 26pin 的原理图可知&#xff0c; Orange Pi Zero 2 可用…

深度学习(CNN+RNN)笔记2

文章目录 第五课&#xff1a;序列模型(Sequence Models)第一周&#xff1a;循环神经网络&#xff08;Recurrent Neural Networks&#xff09;【序列模型、语言模型序列生成、对新序列采样。RNN、GRU、LSTM、双向RNN、深度RNN】第二周&#xff1a;自然语言处理与词嵌入&#xff…

从受众吸引到客户:提高海外社媒转化率的10个技巧!

在当今数字化时代&#xff0c;社交媒体已经成为企业与海外市场互动的关键工具。然而&#xff0c;只有吸引了足够的受众并将他们转化为客户&#xff0c;社交媒体才能实现其真正的潜力。本文Nox聚星将给大家分享提高海外社交媒体的转化率的10个实用技巧&#xff0c;将潜在受众转化…

互联网医院|湖南互联网医院|解决医疗资源不足问题

随着科技的进步和互联网的普及&#xff0c;互联网医院作为一种新型的医疗模式&#xff0c;逐渐受到人们的关注和认可。本文将详细介绍互联网医院的功能和优势&#xff0c;帮助大家全面了解这种新型的医疗服务。 一、互联网医院的功能 1、在线问诊&#xff1a;互联网医院为患者…

[黑马程序员Pandas教程]——Pandas数据结构

目录&#xff1a; 学习目标认识Pandas中的数据结构和数据类型Series对象通过numpy.ndarray数组来创建通过list列表来创建使用字典或元组创建s对象在notebook中不写printSeries对象常用API布尔值列表获取Series对象中部分数据Series对象的运算DataFrame对象创建df对象DataFrame…

漏洞复现--用友NC accept.jsp任意文件上传

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

剑指JUC原理-14.ReentrantLock原理

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

为什么免费证书的有效期为90天

关心SSL证书的朋友们最近可能发现&#xff0c;包括阿里云&#xff0c;亚洲诚信在内的SSL证书服务商&#xff0c;都已经陆续的把之前一年期的免费证书调整为90天有效期&#xff0c;之前的一年期证书价格从免费上涨到几十到几百元不等&#xff0c;这是为什么呢&#xff1f;为什么…

阿里云双十一活动经济型e实例2核2G3M带宽配置云服务器搭建网站教程参考

阿里云2023双十一推出的优惠活动“金秋云创季”&#xff0c;轻量应用服务器2核2G3M带宽只要87元1年&#xff0c;2核4G4M带宽只要165元1年。云服务ECS下的经济型e实例2核2G 3M固定带宽&#xff0c;价格只要99元/1年&#xff0c;新老用户都可购买&#xff0c;同时在2026年3月31日…

19、Python单元测试基础:unittest模块的基本使用

文章目录 创建测试用例测试套件断言运行测试测试固件Python的unittest模块是基于Java的JUnit框架开发出来的,提供了编写和运行单元测试的工具。这篇文章将介绍unittest模块的基本使用,涵盖创建测试用例、测试套件、断言、运行测试以及测试固件的使用。 创建测试用例 在unit…

“菊风Juphoon”邀您莅临11月22-24日CNF南京应急展消防展 | 展位号:115-1

公司简介 菊风依托互联网和电信网音视频融合技术积累&#xff0c;提供智能化的音视频统一通信产品及服务。面向应急管理、消防救援、智慧城市等多个领域&#xff0c;菊风推出适用于全网通的统一通信一体机、统一通信平台。 此外&#xff0c;菊风还提供视频能力平台&#xff0…

2 创建svelte项目(应用程序)

官网方式搭建&#xff1a; npm create sveltelatest my-app cd my-app npm install npm run dev 官网中介绍&#xff1a; 如果您使用的是 VS Code&#xff0c;安装 Svelte for VS Code 就可以了&#xff0c;以便语法高亮显示。 然后&#xff0c;一旦您的项目设置好了&#…

网上书店项目

源码下载地址 支持&#xff1a;远程部署/安装/调试、讲解、二次开发/修改/定制 程序运行视频查看 管理员 图书管理 添加图书 删除图书(可批量删除) 修改图书 查看图书(分页查看) 图书上下架(可批量处理) 图书推荐&#xff08;新品推荐、精品推荐&#xff0c;可批量处理&#…

IGP高级特性简要介绍(OSPF-上篇)

OSPF高级特性 一、OSPF_提升故障收敛及网络恢复速度 1.FRR与BFD快速恢复故障 1.1 FRR 在传统转发模式下&#xff0c;当到达同一个目的网络存在多条路由时&#xff0c;路由器总是选择最优路由使用&#xff0c;并且下发到FIB表指导数据转发。 当最优路由故障时&#xff0c;需…

辐射骚扰整改思路及方法:参数选择与解决之道?|深圳比创达电子EMC

辐射骚扰整改思路及方法&#xff1a;参数选择与解决之道&#xff1f;相信不少人是有疑问的&#xff0c;今天深圳市比创达电子科技有限公司就跟大家解答一下&#xff01; 某产品首次EMC测试时&#xff0c;辐射、静电、浪涌均失败。本篇文章就“参数选择与解决之道”问题进行详细…

Go语言Gin框架前后端分离项目开发工程化实例

文章目录 基本数据配置配置文件管理数据库配置路由配置封装公共方法 数据库模型数据表内容model文件DTO文件 中间件错误异常捕获中间件跨域中间件token认证中间件JWT 控制器UserController 运行调试注册接口登录接口获取用户信息 构建发布项目前端VUE调用接口 基本数据配置 配…

持续集成交付CICD:Jenkins Pipeline与远程构建触发器

目录 一、实验 1.Jenkins Pipeline本地构建触发器 2.Jenkins Pipeline与远程构建触发器&#xff08;第一种方式&#xff09; 3.Jenkins Pipeline与远程构建触发器&#xff08;第二种方式&#xff09; 4.Jenkins Pipeline与远程构建触发器&#xff08;第三种方式&#xff0…