Compose Remember 功能

483 阅读5分钟

Compose 通过调用函数的形式刷新 UI,这些不同的 Compose 函数组合在一起刷新整个界面,称为组合

因为 Compose 还有个响应式的特性,数据组成状态,每当状态发生变化,就会触发这些 Compose 函数重新执行,称之为重组

在现代 Android 架构模型中,如下图所示,状态连接了数据和 UI。

在典型架构中,界面层的界面元素依赖于状态容器,而状态容器又依赖于来自数据层或可选网域层的类。

在典型架构中,界面层的界面元素依赖于状态容器,而状态容器又依赖于来自数据层或可选网域层的类。

状态发生变化就会触发重组。重组具有很多特点,参考上一篇文章 《Compose 编程思想和重组》。

摆脱重组的影响

因为重组会重新执行 Compose 函数,函数中如果创建了对象、或是初始化数据等操作,都会重新执行一遍。

下面是个例子:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var state = "A"

    Text(
        text = "Hello $name$state!",
        modifier = modifier.clickable {
            state = "B"
        }
    )
}

每次重新执行 Greeting 都会重新执行 var state = "A" ,但是,这样直接更新方法中声明的变量是不会触发重组的

重组的更新条件包括:

  1. 任何可观察的状态变化都会触发重组。例如,使用 StateMutableState 持有的状态发生变化时,会触发相关 Composable 函数的重组。
  2. LiveDataFlow 发出新值时,会触发相应 Composable 函数的重组。可以使用 collectAsStateobserveAsState 将这些数据源转换为 Compose 的状态。
  3. 通过 rememberderivedStateOf 等机制管理的外部数据源变化也会触发重组。
  4. 传递给 Composable 函数的参数发生变化时,也会触发重组。

更改此变量不会触发重组的原因是 Compose 并未跟踪此更改

修改代码:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var state = mutableStateOf("A")

    Text(
        text = "Hello $name$state!",
        modifier = modifier.clickable {
            state = "B"
        }
    )
}

使用 mutableStateOf 来使 Compose 函数跟踪此属性的更改,但是这样在点击时仍然不会更新为 B,因为每次重组会重置 state 属性,重新赋值,所以也不会更改为 B。

进一步修改代码:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val state = remember {
        mutableStateOf("A")
    }

    Text(
        text = "Hello $name${state.value}!",
        modifier = modifier.clickable {
            state.value = "B"
        }
    )
}

remember 可以起到保护作用,防止状态在重组时被重置。

另一个很隐蔽的知识点是, remember 的作用域。看看下面的代码:

Column {
  Greeting(
    name = "Android",
    modifier = Modifier.padding(innerPadding)
  )

  Greeting(
    name = "MMA",
    modifier = Modifier.padding(innerPadding)
  )
}

这两个 Greeting 是互不影响的。也就是说 remember 的作用域在可执行函数的内部。

源码分析

我们来看看 remember 函数是如何实现在方法中保存数据的:

@Composable
inline fun <T> remember(crossinline calculation@DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

// ... 这里省略不同个 key 作为参数的同名不同参方法

@Composable
inline fun <T> remember(
    vararg keys: Any?,
    crossinline calculation@DisallowComposableCalls () -> T
): T {
    var invalid = false
    for (key in keys) invalid = invalid or currentComposer.changed(key)
    return currentComposer.cache(invalid, calculation)
}

首先分析最简单的:

@Composable
inline fun <T> remember(crossinline calculation@DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

首先是关键字:

  • inline 说明这是一个内联函数
  • crossinline 确保 calculation 参数不会直接使用 return 结束逻辑,跳过外部函数的后续逻辑。

然后是注解:

  • @Composable:可组合函数内使用。
  • @DisallowComposableCalls: 这将防止在应用该关键字的函数内部发生 Composable 调用。它通常应用于内联 Composable 函数的 lambda 参数,这些参数应该被内联,但不能安全地包含 Composable 调用。

另外这是一个泛型函数,可以根据传入类型输出一样的类型。

最后是重点 currentComposer.cache(false, calculation)

在 Jetpack Compose 中,currentComposer 是一个重要的内部属性,它提供了对当前 Composer 实例的访问。Composer 是 Compose 框架内部用于管理组合过程的核心类,负责跟踪和处理 Compose 的 UI 树。

@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
    @Suppress("UNCHECKED_CAST")
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()
            updateRememberedValue(value)
            value
        } else it
    } as T
}

Composer.cache(...)Composer 的拓展函数,Composer 是一个密封接口(sealed interface)。

这里主要是返回了 rememberedValue 函数的值,并根据 invalid 参数判断值是否失效,失效的话分三步走:

  1. val value = block(),直接执行传入的 block,拿到 block 返回的值;
  2. updateRememberedValue(value),调用 ComposerupdateRememberedValue 函数更新值;
  3. return value,返回更新后的值。

第二步的 updateRememberedValueComposerImpl 中的实现是调用了下面这个函数:

  override fun updateRememberedValue(value: Any?) = updateCachedValue(value)

  @PublishedApi
    @OptIn(InternalComposeApi::class)
    internal fun updateCachedValue(value: Any?) {
        val toStore = if (value is RememberObserver) {
            if (inserting) { changeListWriter.remember(value) }
            abandonSet.add(value)
            RememberObserverHolder(value)
        } else value
        updateValue(toStore)
    }

这里涉及的东西较多,不必深究,只需要知道作用是将插槽表中的当前值安排更新的新值。

回到失效验证,重要的方法是 rememberedValue 函数,这是 Composer 接口中声明的一个方法,实现是:

  override fun rememberedValue(): Any? = nextSlotForCache()

    @PublishedApi
    @OptIn(InternalComposeApi::class)
    internal fun nextSlotForCache(): Any? {
        return if (inserting) {
            validateNodeNotExpected()
            Composer.Empty
        } else reader.next().let {
            if (reusing && it !is ReusableRememberObserver) Composer.Empty
            else if (it is RememberObserverHolder) it.wrapped
            else it
        }
    }

首先,ComposerImpl 有一个插槽表,slotTable 构造参数:

internal class ComposerImpl(
    /**
     * An adapter that applies changes to the tree using the Applier abstraction.
     */
    override val applier: Applier<*>,

    /**
     * Parent of this composition; a [Recomposer] for root-level compositions.
     */
    private val parentContext: CompositionContext,

    /**
     * The slot table to use to store composition data
     */
    private val slotTable: SlotTable,

    private val abandonSet: MutableSet<RememberObserver>,

    private var changes: ChangeList,

    private var lateChanges: ChangeList,

    /**
     * The composition that owns this composer
     */
    override val composition: ControlledComposition
) : Composer 

SlotTable 在 Jetpack Compose 中是一个关键的数据结构,用于存储和管理组合过程中的数据。它支持状态管理、数据生命周期管理以及高效的数据访问。

SlotTable 内容繁多,会单独写一篇文章来讲。还是分析一下 nextSlotForCache,点到为止。

首先这是用来返回一个值的,他会根据插入状态来判断,

  • 插入中,首先使用 validateNodeNotExpected 确保节点状态正常,然后返回一个 Composer.Empty

  • 其他状态(没有在插入),通过 SlotReader 读取下一个节点

    • 然后判断如果 reusing (复用)为 true 并且获取到的值 it 不是 ReusableRememberObserver 类型,则返回 Composer.Empty

    • 否则,

      • 如果获取到的值 itRememberObserverHolder 类型,则返回其包装的值 it.wrapped
      • 否则直接返回节点本身

ReusableRememberObserver: 可重用的记忆观察者,在组合重用/停用期间不会被移除。用于在组合停用期间保留组合局部变量。