Compose 实践与探索十九 —— Compose 显示原理浅析

本篇是 Compose 系列的最后一篇文章,我们来了解一下 Compose UI 的显示与刷新原理。

UI 要显示到界面上,通常可以分为两步:设置要显示的内容、程序运行到相应的生命周期显示该内容。

1、设置显示内容

使用原生的 View 系统构建 UI 时,都是在 Activity 的 onCreate() 中调用 setContentView() 设置 Activity 的显示内容。到了 Compose 就改为调用 ComponentActivity 的扩展函数 setContent()。实际上目的都是为了设置 Activity 的根视图,也就是要显示的内容:

    /**
     * 将给定的可组合项合成到给定的 Activity 中。[content] 将成为给定 Activity 的根视图。
     * 这大致相当于使用一个 [ComposeView] 调用 [ComponentActivity.setContentView]:
     *
     * setContentView(
     *   ComposeView(this).apply {
     *     setContent {
     *       MyComposableContent()
     *     }
     *   }
     * )
     * 
     * @param parent 父组合(parent composition)的引用,用于协调组合更新的调度
     * @param content 一个声明 UI 内容的 @Composable 函数
     */ 
    public fun ComponentActivity.setContent(
        parent: CompositionContext? = null,
        content: @Composable () -> Unit
    ) {
        // 1.复用已经存在的 ComposeView:将 DecorView 中 id 为 android.R.id.content 的 ViewGroup
        // 的第一个子组件转换成 ComposeView。因为最下面 setContentView() 设置的就是它。
        val existingComposeView = window.decorView
            .findViewById<ViewGroup>(android.R.id.content)
            .getChildAt(0) as? ComposeView
        
        // 2.existingComposeView 若已存在
        if (existingComposeView != null) with(existingComposeView) {
            // 此处调用的是 ComposeView 的 setContent(),因此不会形成递归
            // 2.1 更新组合上下文关系
            setParentCompositionContext(parent)
            // 2.2 替换现有的 Compose 内容
            setContent(content)
        } else ComposeView(this).apply { // 3.existingComposeView 若不存在
            // 3.1 设置组合上下文以及 Compose 内容
            setParentCompositionContext(parent)
            setContent(content)
            // 3.2 生命周期配置:绑定生命周期所有者、绑定 SavedStateRegistry 等
            // AppCompat 1.3+ 版本之前由于 bug 不会自动设置 Owners,所以才在这里手动设置
            setOwners()
            // 设置当前的 ComposeView 为 Activity 的根视图
            setContentView(this, DefaultActivityContentLayoutParams)
        }
    }

setContent() 会将 Compose 组件设置为 Activity 的根视图,具体说来:

  • 将 Compose 组件封装进 ComposeView 中,然后用 ComponentActivity 的 setContentView() 将这个 ComposeView 设置为 Activity 的根视图
  • 如果已经使用 setContentView() 设置过 Activity 的根视图,那么 existingComposeView 就能获取到这个 ComposeView,复用可以避免重复创建从而优化性能

接下来我们梳理一下 setContent() 的内容细节。

在这里插入图片描述

首先解释一下 existingComposeView。

我们都知道 Activity 的 DecorView 中的 android.R.id.content 是一个 ViewGroup(也就是上图紫色的 R.layout.xxx 部分),用来展示 Activity 的根视图。对这个 ViewGroup 调用 getChildAt(0) 就是去拿这个 ViewGroup 内的根组件,这个根组件是在最下方通过 setContentView() 设置的,就是将 ComposeView 按照 DefaultActivityContentLayoutParams 添加到 ViewGroup 内。因此,第一次进入 setContent() 时,由于还没有调用 setContentView() 设置过 Activity 的根视图,此时 的 existingComposeView 就是 null,会执行注释中第 3 步的流程。在这一次调用过 setContentView() 之后,existingComposeView 就不是 null 了。后续如果发生配置变化(如屏幕旋转),重新创建 Activity 再次进入 setContent() 时,existingComposeView 可以被复用而无需重新创建 ComposeView。

然后我们看在 2、3 两步都调用了的两个函数 —— ComposeView 的 setParentCompositionContext() 和 setContent()。

setParentCompositionContext() 用于设置父组合上下文(用于管理 Composition 的作用域和生命周期),是 ComposeView 的父类 AbstractComposeView 中的函数:

    /**
     * 设置应该作为此视图的组合(view's composition)的父级的 CompositionContext。如果 parent 为 null,
     * 则将自动从视图所依附到的窗口(the window the view is attached to)确定。
     */
    fun setParentCompositionContext(parent: CompositionContext?) {
        parentContext = parent
    }

CompositionContext 用于连接父组合与子组合,参数的 parent 是由 setContent() 的第一个参数传到这里的。由于调用 setContent() 时通常不会给 parent 参数赋值,因此它会取默认值 null,所以这里给 parentContext 赋的就是 null。至于 parentContext 属性的具体内容,我们暂时还用不到,因此暂时搁置。

然后我们再看 ComposeView 的 setContent(),其作用是设置 Compose UI 内容:

    // 保存页面要展示的 Composable 组件函数的状态
	private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
    
    /**
     * 设置此视图的 Jetpack Compose UI 内容。当视图附加到窗口或调用 createComposition 时,将发生
     * 初始的组合(initial composition),以先到者为准。
     * @param content 就是在 Activity 的 onCreate() 中调用的 setContent() 的尾随 lambda,页面内容
     */
    fun setContent(content: @Composable () -> Unit) {
        // 这个变量表示是否应该在附加到窗口时创建新的组合,后面会用到
        shouldCreateCompositionOnAttachedToWindow = true
        // 将参数传入的表示 UI 内容的 Composable 函数保存到 content 中
        this.content.value = content
        // 我们现在看的是 onCreate() 中的流程,还没到 onAttachedToWindow(),因此此时 
        // isAttachedToWindow 为 false,不会进入 if,但是 createComposition() 内的主要逻辑 
        // ensureCompositionCreated() 会在附加到 Window 的过程 onAttachedToWindow() 中被调用
        if (isAttachedToWindow) {
            createComposition()
        }
    }

setContent() 流程走完了,按照当前流程就是设置了一个布尔值,保存了 Activity 要显示的 Composable 组件函数到 content 属性中,并没有看出如何显示这个 content 的。这是因为 setContent() 只负责设置 Activity 根视图的 ComposeView,而显示需要等到被设置为 Activity 根视图的 ComposeView 的生命周期走到 onAttachedToWindow() 时才会显示。

2、创建组合

ComposeView 是 AbstractComposeView 这个 ViewGroup 的子类,那也就是 View 的子类。一个 View 只有在生命周期走到 onAttachedToWindow(),也就是其附加到窗口时才会显示:

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // 记录与当前页面绑定的 Window
        previousAttachedWindowToken = windowToken
        
        if (shouldCreateCompositionOnAttachedToWindow) {
            // 确保 Composition 被创建
            ensureCompositionCreated()
        }
    }

shouldCreateCompositionOnAttachedToWindow 表示是否应该在走到 onAttachedToWindow() 时创建 Composition,在前面执行 ComposeView 的 setContent() 时已经被设为 true 了,所以这里可以进入 if 执行 ensureCompositionCreated():

    private fun ensureCompositionCreated() {
        // 还没执行过组合过程,因此 composition 为 null
        if (composition == null) {
            try {
                // 标记,是否正在创建组合,防止重入
                creatingComposition = true
                // 进行组合过程并返回组合结果
                composition = setContent(resolveParentCompositionContext()) {
                    Content()
                }
            } finally {
                // 创建完组合,清除标记
                creatingComposition = false
            }
        }
    }

这里调用的是父类 AbstractComposeView 的 setContent 函数(双参),而不是此前看过的子类的 ComposeView 的 setContent 函数(单参,并且父类中也没办法调用子类的函数,除非就是子类对象调用了一个父子都有的函数),目的就是为了创建一个组合对象 Composition:

internal fun AbstractComposeView.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    // 1.启动全局快照监听
    GlobalSnapshotManager.ensureStarted()
    // 2.视图复用逻辑
    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
    // 3.实际创建组合
    return doSetContent(composeView, parent, content)
}

创建组合的过程主要做了三件事:

  1. 启动全局快照监听
  2. 尝试复用 AndroidComposeView,如没有则创建一个,并将其以 DefaultLayoutParams(宽高都 Wrap) add() 到 ComposeView 中
  3. 调用 doSetContent() 创建 Composition 对象并返回

在看 setContent() 的具体内容之前,先看它的两个参数的情况。

2.1 setContent() 的参数

调用端在调用 setContent() 时给它的两个参数传的是 resolveParentCompositionContext() 和 { Content() },分别追这两个参数的代码。

resolveParentCompositionContext()

resolveParentCompositionContext() 通过四级递进策略解析父组合的上下文,确保 Compose 组件能正确接入 Android 视图树的上下文体系:

/**
 * 确定正确的 [CompositionContext] 作为当前视图组合的父级上下文。
 * 该方法会缓存查找到的上下文供后续使用(详见 [cachedViewTreeCompositionContext])。
 * 
 * 若存在 [cachedViewTreeCompositionContext],但 [findViewTreeCompositionContext] 无法找到
 * 父级上下文,则优先使用已缓存的上下文(如果有效),而不是直接使用 [windowRecomposer]
 * (因为后者可能延迟创建 recomposer)。
 * 
 * 当视图被重新附加到同一窗口,且 [findViewTreeCompositionContext] 无法找到 [windowRecomposer] 
 * 提供的上下文时,可能出现这种情况:视图处于某个动画覆盖层(如消失动画),此时仍需保持组合/重组能力,
 * 避免新建 recomposer,同时确保能定位视图树依赖。
 */
private fun resolveParentCompositionContext() = parentContext
    ?: findViewTreeCompositionContext()?.cacheIfAlive()
    ?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive }
    ?: windowRecomposer.cacheIfAlive()

第一优先级是显式指定的父级上下文 parentContext,在看 ComponentActivity.setContent() 的流程时出现过,在 setParentCompositionContext() 内被赋值,通常是 null。

第二优先级是 findViewTreeCompositionContext() 去视图树进行实时查找,找到距当前视图最近的 CompositionContext:

/**
 * 返回此视图层级结构中当前节点的父组合上下文(parent CompositionContext),如果未找到则返回 null。
 * 如需获取或设置特定视图的父组合上下文,请参阅 compositionContext。
 */
fun View.findViewTreeCompositionContext(): CompositionContext? {
    // 1.先看自己的 compositionContext
    var found: CompositionContext? = compositionContext
    if (found != null) return found
    // 2.如果自己的 compositionContext 为 null,就向上找父级的 compositionContext
    var parent: ViewParent? = parent
    while (found == null && parent is View) {
        found = parent.compositionContext
        parent = parent.getParent()
    }
    return found
}

当前 View 的 compositionContext 与父级组件的 compositionContext 都是 View 的扩展属性:

/**
 * 这个属性表示视图层级中当前视图及其子视图使用的 [CompositionContext](组合上下文)。
 * 设置为非 null 值可为子视图的组合提供自定义上下文;设置为 null 则回退到使用祖先视图提供的上下文。
 */
var View.compositionContext: CompositionContext?
    get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext
    set(value) {
        setTag(R.id.androidx_compose_ui_view_composition_context, value)
    }

通常不会给 View 设置 id 为 androidx_compose_ui_view_composition_context 的 Tag,所以这一步的 compositionContext 一般为 null。

但假如 findViewTreeCompositionContext() 找到了 CompositionContext,那么就执行 cacheIfAlive():

    /**
     * 如果这个 CompositionContext 处于活跃状态,将它缓存到 cachedViewTreeCompositionContext 中同时
     * 返回自身 
     */
    private fun CompositionContext.cacheIfAlive(): CompositionContext = also { context ->
        context.takeIf { it.isAlive }
            ?.let { cachedViewTreeCompositionContext = WeakReference(it) }
    }

当 CompositionContext 处于活跃状态时,将其缓存到 cachedViewTreeCompositionContext 中同时返回它。

第三优先级正是从 cachedViewTreeCompositionContext 这个弱引用中去获取 CompositionContext,如果其处于活动状态就使用它。

第四优先级是检查 windowRecomposer 并在它处于活跃状态时缓存它,它也是 View 的扩展属性:

/**
 * 获取或延迟创建与此视图所在窗口关联的 [Recomposer]。调用此属性时,视图必须已附加到窗口,
 * 且窗口的根视图已通过 [findViewTreeLifecycleOwner] 注册了 [LifecycleOwner]。
 */
@OptIn(InternalComposeUiApi::class)
internal val View.windowRecomposer: Recomposer
    get() {
        check(isAttachedToWindow) {
            "Cannot locate windowRecomposer; View $this is not attached to a window"
        }
        // 获取窗口内容根视图
        val rootView = contentChild
        return when (val rootParentRef = rootView.compositionContext) {
            // 无父级上下文时创建并安装 Recomposer
            null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)
            // 已有 Recomposer 直接复用
            is Recomposer -> rootParentRef
            // 类型不匹配时抛出错误
            else -> error("root viewTreeParentCompositionContext is not a Recomposer")
        }
    }

contentChild 就是 android.R.id.content 对应的 ViewGroup 的直接子视图,也就是 ComponentActivity.setContent() 中看到的那个 ComposeView —— existingComposeView:

/**
 * 查找当前视图的内容子视图(content child)。内容子视图指以下两种之一:
 * 1. ID 为 [android.R.id.content] 的视图的直接子视图(即通过 Activity 或 Dialog 
 * 的 setContentView 设置的内容视图)
 * 2. 窗口的根视图
 * 不使用 [View.getRootView] 的原因是:Android 框架会在 Activity 重建时重用窗口装饰视图(DecorView)。
 * 由于窗口重组器(window recomposer)与宿主 Activity 的生命周期绑定,当 Activity 实例重建时,
 * 需要让旧的 recomposer 关闭,并为新 Activity 实例创建新的 recomposer。
 */
private val View.contentChild: View
    get() {
        var self: View = this
        var parent: ViewParent? = self.parent
        while (parent is View) {
            if (parent.id == android.R.id.content) return self
            self = parent
            parent = self.parent
        }
        return self
    }

将 contentChild 属性赋值给 rootView 后,根据 rootView.compositionContext 决定走哪一条分支。前面提过,现在所有 View 的 CompositionContext 都是 null,因此要调用 WindowRecomposerPolicy.createAndInstallWindowRecomposer():

@InternalComposeUiApi
object WindowRecomposerPolicy {

    private val factory = AtomicReference<WindowRecomposerFactory>(
        WindowRecomposerFactory.LifecycleAware
    )

    internal fun createAndInstallWindowRecomposer(rootView: View): Recomposer {
        val newRecomposer = factory.get().createRecomposer(rootView)
        rootView.compositionContext = newRecomposer

        // 界面退出后做清理收尾工作的代码,省略...

        return newRecomposer
    }
}

该函数通过 WindowRecomposerFactory 工厂的 createRecomposer() 创建了 CompositionContext 的子类对象 Recomposer,并将该对象赋值给 rootView 的 compositionContext 属性。至此,rootView 的 compositionContext 终于不为空了。

下面我们要看一下 createRecomposer() 是如何创建 CompositionContext 的,点进函数源码发现是 WindowRecomposerFactory 的接口函数:

/**
 * 用于创建 Android 窗口作用域重组器(Recomposer)的工厂接口。详见 createRecomposer。
 */
@InternalComposeUiApi
fun interface WindowRecomposerFactory {
    /**
     * 为指定窗口创建 [Recomposer],[windowRootView] 是该窗口视图层级的根视图。
     * 工厂需定义重组器的关闭策略(通过 [Recomposer.cancel])。
     * [windowRootView] 将持有返回的 [Recomposer] 的强引用,直到其关闭后完成 [join]。
     */
    fun createRecomposer(windowRootView: View): Recomposer

    companion object {
        /**
         * 创建生命周期感知的 [Recomposer] 的工厂实现。
         * 
         * 返回的 [Recomposer] 将绑定到视图层级根视图的 [LifecycleOwner],
         * 并在窗口 UI 线程的 [AndroidUiDispatcher.CurrentThread] 上运行重组和组合副作用。
         * 关联的 [MonotonicFrameClock] 仅在生命周期至少为 [STARTED] 时产生帧,
         * 使得动画等使用帧时钟的 API 在窗口不可见时暂停。
         */
        @OptIn(ExperimentalComposeUiApi::class)
        val LifecycleAware: WindowRecomposerFactory = WindowRecomposerFactory { rootView ->
            rootView.createLifecycleAwareWindowRecomposer()
        }
    }
}

此时就要看 factory.get() 拿到的是什么对象,通过 factory 的定义可以确定 get() 得到的是一个正是该接口伴生对象内定义的 LifecycleAware,createLifecycleAwareWindowRecomposer() 会返回一个已经准备好(界面刷新)的工具 Recomposer:

@ExperimentalComposeUiApi
fun View.createLifecycleAwareWindowRecomposer(
    coroutineContext: CoroutineContext = EmptyCoroutineContext,
    lifecycle: Lifecycle? = null
): Recomposer {
    val baseContext = if (coroutineContext[ContinuationInterceptor] == null ||
        coroutineContext[MonotonicFrameClock] == null
    ) {
        AndroidUiDispatcher.CurrentThread + coroutineContext
    } else coroutineContext
    val pausableClock = baseContext[MonotonicFrameClock]?.let {
        PausableMonotonicFrameClock(it).apply { pause() }
    }

    ...
    val motionDurationScale = baseContext[MotionDurationScale] ?: MotionDurationScaleImpl().also {
        systemDurationScaleSettingConsumer = it
    }

    val contextWithClockAndMotionScale =
        baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
    val recomposer = Recomposer(contextWithClockAndMotionScale)
    ...
    return recomposer
}

所以,resolveParentCompositionContext() 最终返回的是一个用于进行重组刷新的工具 Recomposer。

Content()

再看 setContent() 的第二个参数,传的是 { AbstractComposeView.Content() }:

    @Composable
    @UiComposable
    abstract fun Content()

在它的子类 ComposeView 中的实现就是调用此前保存在 content 属性中的函数:

    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)

    @Composable
    override fun Content() {
        content.value?.invoke()
    }

把 Content() 放入 lambda 表达式中作为 setContent() 的第二个参数,执行该 lambda 就是执行 ComponentActivity.setContent() 传入的 Composable 组件函数。它与第一个参数 resolveParentCompositionContext() 一样,都会传入第 3 步的 doSetContent() 中。接下来我们就开始介绍 setContent() 中的 3 个步骤。

2.2 启动快照全局监听

先看 setContent() 的第 1 步,调用 GlobalSnapshotManager 的 ensureStarted() 启动全局快照监听:

/**
 * 平台特定的全局快照状态写入监听机制,用于调度定期发送快照应用通知。
 * 该机制需保持平台特定性,与特定平台和框架目标的线程模型及更新机制紧密相关。
 * 
 * 特定平台/框架的组合引导机制应在初始化时调用 [ensureStarted] 方法,
 * 以启用全局快照通知。在 Android 平台上,这些通知始终通过 [AndroidUiDispatcher.Main] 发送,
 * 其他平台可采用不同的策略。
 */
internal object GlobalSnapshotManager {
    // 原子布尔值确保单次初始化
    private val started = AtomicBoolean(false)

    // 启动全局快照监听
    fun ensureStarted() {
        if (started.compareAndSet(false, true)) {
            // 创建合并型 Channel(仅保留最新事件)
            val channel = Channel<Unit>(Channel.CONFLATED)
            // 在 Android 主线程创建协程作用域
            CoroutineScope(AndroidUiDispatcher.Main).launch {
                // 持续消费 Channel 事件
                channel.consumeEach {
                    // 发送快照应用通知
                    Snapshot.sendApplyNotifications()
                }
            }
            // 注册全局写入观察者
            Snapshot.registerGlobalWriteObserver {
                // 触发 Channel 发送事件
                channel.trySend(Unit)
            }
        }
    }
}

Snapshot 系统是用来管理 Compose 组件依赖的状态的,它会在这些状态发生变化时自动感知到它们的变化,并把这些变化应用到界面中,保证使用这些状态最新的值去及时的刷新界面。具体的做法就是在上面的代码中,Snapshot 通过 registerGlobalWriteObserver() 注册了全局的写值观察者,这样 Snapshot 在监听到组件依赖的变量发生变化时,就会执行 Channel 的 trySend() 去生产事件。消费端是在 Android 主线程启动的协程中通过 consumeEach() 持续地接收事件,调用 Snapshot 的 sendApplyNotifications() 通知有状态发生了更新。

此外,Snapshot 还支持多线程,可以在多个线程中去更新 Compose 组件依赖的状态,而原生只能在主线程中更新 UI。

通知状态的变化

正如上面所说,当状态值发生变化时,会在 Channel 消费事件时调用 Snapshot 的 sendApplyNotifications() 发送通知:

sealed class Snapshot {
    // 发生了变化的状态集合,不为 null 时说明有状态更新了
    override var modified: MutableSet<StateObject>? = null
    companion object {
        fun sendApplyNotifications() {
            val changes = sync {
                // currentGlobalSnapshot 是原子引用 AtomicReference,里面包着 GlobalSnapshot
                currentGlobalSnapshot.get().modified?.isNotEmpty() == true
            }
            if (changes)
                advanceGlobalSnapshot()
        }
    }
}

当组件依赖的状态有变化时,会调用 advanceGlobalSnapshot():

private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }

private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    // 原子获取当前全局快照 GlobalSnapshot
    var previousGlobalSnapshot = snapshotInitializer as GlobalSnapshot
    val result = sync {
        previousGlobalSnapshot = currentGlobalSnapshot.get()
        // 创建新快照并执行回调
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    // 1.检查是否有修改的状态
    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        // 2.获取观察者列表的快照副本
        val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
        // 3.遍历观察者进行状态的应用
        // 把界面中用到的发生变化的状态进行新值的应用。确切地说,把界面中使用到的发生状态变化的
        // 部分标记为失效,这样失效的部分就会发生重组,然后再发生布局和绘制,完成整个页面的刷新。
        // 只标记发生变化的部分是为了节省性能,只刷新应该刷新的地方保证耗时最短,刷新效率最高
        observers.fastForEach { observer ->
            observer(modified, previousGlobalSnapshot)
        }
    }

    // 清理无效记录
    sync {
        modified?.forEach(::overwriteUnusedRecordsLocked)
    }

    return result
}

观察者列表 applyObservers 是在 registerApplyObserver() 中被添加的:

        // 还是在 Snapshot 的伴生对象中
        fun registerApplyObserver(observer: (Set<Any>, Snapshot) -> Unit): ObserverHandle {
            // Ensure observer does not see changes before this call.
            advanceGlobalSnapshot(emptyLambda)

            sync {
                // 向 applyObservers 添加监听者
                applyObservers.add(observer)
            }
            return ObserverHandle {
                sync {
                    applyObservers.remove(observer)
                }
            }
        }

registerApplyObserver() 在 Recomposer 的 recompositionRunner() 中被调用:

    private suspend fun recompositionRunner(
        block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit
    ) {
        withContext(broadcastFrameClock) {
            ...
            // 注册回调,收到回调之后,通过 resume() 开始执行重组工作
            val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
                synchronized(stateLock) {
                    if (_state.value >= State.Idle) {
                        snapshotInvalidations.addAll(changed)
                        deriveStateLocked()
                    } else null
                }?.resume(Unit)
            }
            ...
        }
    }

总结一下上述过程:

  • Snapshot 快照系统通过 registerGlobalWriteObserver() 设置全局监听,当有状态进行写行为时,就会触发监听回调让 Channel 生产事件
  • Channel 消费端在得知状态发生变化后,执行 Snapshot 的 sendApplyNotifications() 通知状态发生了变化,触发重组

上述过程利用 Channel 的特性对重组触发次数做了优化,一帧中只会触发一或两次重组,从而避免导致高频修改引发高频刷新的性能问题。

知识贯通

这里要插入一点,因为我们刚刚看到了 Snapshot 是如何注册对 State 进行全局写监听的,所以我们要回到起点,看看当 State 发生变化时,Snapshot 会做怎样的处理。

通过 State 的 set(value) 为其赋新值时:

internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if (!policy.equivalent(it.value, value)) {
                next.overwritable(this, it) { this.value = value }
            }
        }

next 是 StateStateRecord,它的 overwritable() 会提供一个可写的 StateRecord:

/**
 * 对给定的状态记录调用 [block] 函数,并为其提供一个可写的状态记录。假定这是针对状态对象中的
 * 第一个状态记录调用的。若该记录是在当前可变快照中创建的,则视为可写。此方法仅应在记录的
 * 所有字段将被完全覆盖时使用(例如记录只有一个字段且该字段将被写入)。
 *
 * 警告:如果调用者未覆盖状态记录中的所有字段,将导致对象状态不一致,未写入的字段几乎肯定会处于
 * 错误状态。若 [block] 可能不会写入所有字段,请改用 [writable] 方法。
 *
 * @param state 包含此记录的状态对象
 * @param candidate 由 [withCurrent] 返回的快照记录的当前候选记录
 * @param block 将修改记录所有字段的操作块
 */
internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R
): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.overwritableRecord(state, snapshot, candidate).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}

首先 snapshot 被初始化为一个 GlobalSnapshot:

internal val snapshotInitializer: Snapshot = currentGlobalSnapshot.get()

private val currentGlobalSnapshot = AtomicReference(
    GlobalSnapshot(
        id = nextSnapshotId++,
        invalid = SnapshotIdSet.EMPTY
    ).also {
        openSnapshots = openSnapshots.set(it.id)
    }
)

然后在 sync 内对其进行原子性覆盖,覆盖为当前线程快照:

 	// Snapshot 的伴生对象
	companion object {
        // 返回线程的活动快照。如果没有活动状态的线程快照,则使用当前的全局快照
        val current get() = currentSnapshot()
    }

internal fun currentSnapshot(): Snapshot =
    threadSnapshot.get() ?: currentGlobalSnapshot.get()

全局快照 GlobalSnapshot 继承自 MutableSnapshot,它的第 4 个参数 writeObserver 就是对 State 的写监听:

internal class GlobalSnapshot(id: Int, invalid: SnapshotIdSet) :
    MutableSnapshot(
        id, invalid, null,
        // writeObserver
        sync {
            (
                if (globalWriteObservers.isNotEmpty()) {
                    globalWriteObservers.toMutableList()
                } else null
                )?.let {
                it.singleOrNull() ?: { state: Any ->
                    it.fastForEach { it(state) }
                }
            }
        }
    ) 

overwritable() 内执行 notifyWrite() 时就会调用这个 writeObserver 的函数内容:

internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}

2.3 生成 AndroidComposeView

再看 setContent() 的第 2 部分:

    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }

生成一个 AndroidComposeView 并将其添加到当前的 ComposeView 中,形成一个 ComposeView 包含 AndroidComposeView 的结构。实际上,AndroidComposeView 才是负责显示与触摸反馈(真正干活的)的那个 View。

2.4 创建 Composition 对象

setContent() 的最后一部分是 doSetContent():

private fun doSetContent(
    owner: AndroidComposeView,
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    ...
    val original = Composition(UiApplier(owner.root), parent)
    val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
        as? WrappedComposition
        ?: WrappedComposition(owner, original).also {
            owner.view.setTag(R.id.wrapped_composition_tag, it)
        }
    wrapped.setContent(content)
    return wrapped
}

doSetContent() 主要做了三件事:

  1. 创建一个 Composition 对象:传入 Composition 要使用的 Applier 与父组件的 CompositionContext,得到 CompositionImpl 类型的 original
  2. 获取 WrappedComposition 对象 wrapped:取 AndroidComposeView 的 id 为 wrapped_composition_tag 的 Tag,如果获取不到就用 WrappedComposition 的构造函数将 AndroidComposeView 与 CompositionImpl 包装起来,并把新生成的 WrappedComposition 设置给自己的 Tag
  3. 调用 WrappedComposition 的 setContent() 设置要显示的 Composable 组件函数

调用 WrappedComposition 的 setContent():

// 本流程中传入的 original 的实际类型是 CompositionImpl
private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver {

    private var disposed = false
    private var addedToLifecycle: Lifecycle? = null
    private var lastContent: @Composable () -> Unit = {}
    
	override fun setContent(content: @Composable () -> Unit) {
        // 1.对 AndroidComposeView 设置监听,当 ViewTreeOwners 可用时回调
        owner.setOnViewTreeOwnersAvailable {
            if (!disposed) {
                val lifecycle = it.lifecycleOwner.lifecycle
                // 将要显示的 Composable 组件内容保存到 lastContent 中
                lastContent = content
                // 2.先进入 if,再进入 else if
                if (addedToLifecycle == null) {
                    addedToLifecycle = lifecycle
                    // this will call ON_CREATE synchronously if we already created
                    lifecycle.addObserver(this)
                } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                    // original 是 CompositionImpl
                    original.setContent {
                        ...
                    }
                }
            }
        }
    }
}

setContent() 主要有两点:

  1. AndroidComposeView.setOnViewTreeOwnersAvailable() 设置 ViewTreeOwners 可用的监听,当 AndroidComposeView 的 viewTreeOwners 属性不为空时回调,该属性在 onAttachedToWindow() 内被赋值,所以可以大致认为是对 onAttachedToWindow() 的监听
  2. addedToLifecycle 初始为 null,那么首先会进入 if 条件,为当前 AndroidComposeView 的 lifecycle 添加监听,调用 addObserver() 后生命周期满足 else if 的条件,因此又会进入 else if 执行 Composition 的 setContent()

Composition 的 setContent():

    override fun setContent(content: @Composable () -> Unit) {
        check(!disposed) { "The composition is disposed" }
        // 将参数的 Composable 函数保存到成员属性 composable 中
        this.composable = content
        // parent 的声明类型是 CompositionContext,实际类型是 2.1 节中的
        // resolveParentCompositionContext() 返回值 Recomposer
        parent.composeInitial(this, composable)
    }

看 Recomposer 的 composeInitial():

 	// Compose 运行时中执行初始组合(Initial Composition)的核心方法
	internal override fun composeInitial(
        composition: ControlledComposition,
        content: @Composable () -> Unit
    ) {
        // 记录组合前的状态
        val composerWasComposing = composition.isComposing
        try {
            // 核心组合执行
            composing(composition, null) {
                composition.composeContent(content)
            }
        } catch (e: Exception) {
            // 错误处理
            processCompositionError(e, composition, recoverable = true)
            return
        }

        // 快照系统通知
        if (!composerWasComposing) {
            Snapshot.notifyObjectsInitialized()
        }

        // 组合实例注册
        synchronized(stateLock) {
            if (_state.value > State.ShuttingDown) {
                if (composition !in knownCompositions) {
                    knownCompositions += composition
                }
            }
        }
        ...
        // 应用变更
        try {
            composition.applyChanges()
            composition.applyLateChanges()
        } catch (e: Exception) {
            processCompositionError(e)
            return
        }

        // 二次快照通知
        if (!composerWasComposing) {
            // Ensure that any state objects created during applyChanges are seen as changed
            // if modified after this call.
            Snapshot.notifyObjectsInitialized()
        }
    }

看核心组合执行过程,在 composing() 的尾随 lambda 中执行 composition.composeContent(),就有调用链 CompositionImpl.composeContent() -> ComposerImpl.composeContent() -> ComposerImpl.doCompose():

    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
            ...
            try {
                ...
                observeDerivedStateRecalculations(
                    start = {
                        childrenComposing++
                    },
                    done = {
                        childrenComposing--
                    },
                ) {
                    if (content != null) {
                        startGroup(invocationKey, invocation)
                        // 关键代码
                        invokeComposable(this, content)
                        endGroup()
                    }
                }
                ...
            }
        }
    }

invokeComposable() 执行的是 ActualJvm.jvm.kt 中的函数:

internal actual fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
    @Suppress("UNCHECKED_CAST")
    val realFn = composable as Function2<Composer, Int, Unit>
    realFn(composer, 1)
}

composable 参数是一个空参的 Composable 函数,它被强转为 Function2,是因为 Compose 编译器插件会对所有 @Composable 函数添加两个参数。最后调用转换成 Function2 的 realFn(),也就是执行 lambda 内的内容了。

以上就是 setContent() 的内容,它并没有对界面的显示做任何工作,它所做的就是布置好各种监听器,以便变量发生变化时触发界面刷新。

3、组件显示

真正负责显示的是各个组件 Composable 函数内的通用函数 Layout():

@UiComposable
@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        // 这里预先设置要做的处理策略
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

ReusableComposeNode 内会执行 Composer 的 createNode() 创建 LayoutNode 节点:

@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
        // 创建 LayoutNode 节点
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    // 更新 LayoutNode 节点
    Updater<T>(currentComposer).update()
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

createNode() 会执行 factory 参数生成 LayoutNode,并把该节点插进 LayoutNode 树中:

    // ComposerImpl:
    @Suppress("UNUSED")
    override fun <T> createNode(factory: () -> T) {
        validateNodeExpected()
        runtimeCheck(inserting) { "createNode() can only be called when inserting" }
        val insertIndex = nodeIndexStack.peek()
        val groupAnchor = writer.anchor(writer.parent)
        groupNodeCount++
        recordFixup { applier, slots, _ ->
            @Suppress("UNCHECKED_CAST")
            // 创建 LayoutNode,看参数来源,是 ReusableComposeNode() 的 factory 参数
            val node = factory()
            // LayoutNode 装进 SlotTable 中
            slots.updateNode(groupAnchor, node)
            @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
            nodeApplier.insertTopDown(insertIndex, node)
            applier.down(node)
        }
        recordInsertUpFixup { applier, slots, _ ->
            @Suppress("UNCHECKED_CAST")
            val nodeToInsert = slots.node(groupAnchor)
            applier.up()
            @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<Any?>
            // 将 LayoutNode 节点插进 LayoutNode 树中(树是组合的最终结果)
            nodeApplier.insertBottomUp(insertIndex, nodeToInsert)
        }
    }

这样就是每个组件在执行到其内部的 Layout() 时都会将这个组件生成为一个 LayoutNode 并构建 LayoutNode 树。

SlotTable 是一种数据结构,用于存储 Compose 的 LayoutNode 树以及这个树上用到的变量。在显示界面时,是用不到 SlotTable 的。它在最底层是用一维数组实现了 LayoutNode 树的存储,而对于各个嵌套的 LayoutNode 之间是使用链表将它们连接起来的。使用它的目的是为了提升性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值