当前位置: 首页 > news >正文

Compose 原理解析

Compose 的组件都是放在 setContent() 之后才能显示的,那需要先看看这个函数的作用。

先看 ComponentActivity 的扩展函数 setContent():

    /*** 将给定的可组合项合成到给定的 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) {// 将 DecorView 中 id 为 android.R.id.content 的 ViewGroup 的第一个子组件转换成 ComposeViewval existingComposeView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeViewif (existingComposeView != null) with(existingComposeView) {// 此处调用的是 ComposeView 的 setContent(),因此不会形成递归setParentCompositionContext(parent)setContent(content)} else ComposeView(this).apply {// Set content and parent **before** setContentView// to have ComposeView create the composition on attachsetParentCompositionContext(parent)setContent(content)// Set the view tree owners before setting the content view so that the inflation process// and attach listeners will see them already present// AppCompat 1.3+ 版本之前由于 bug 不会自动设置 Owners,所以才在这里手动设置,属于修复 bug 的代码setOwners()// 设置当前的 ComposeView 为 Activity 的显示内容setContentView(this, DefaultActivityContentLayoutParams)}}

setContent() 的主要任务是将用户写的 Compose 组件设置为 Activity 的显示内容。具体说来,就是将 Compose 组件封装进 ComposeView 中,然后用 ComponentActivity 的 setContentView() 设置当前 Activity 显示这个 ComposeView。在这个过程中,通过复用现有的 ComposeView —— existingComposeView 避免重复创建从而优化了性能。

关于 existingComposeView,是将 DecorView 中的 android.R.id.content 对应的 ViewGroup 的第一个子组件强转为 ComposeView。熟悉原生的 Activity 布局层级的朋友们应该知道,android.R.id.content 对应的 ViewGroup 就是显示 Activity 的整体内容的,也就是显示原生的 XML 布局的。通过 getChildAt(0) 去拿它的第一个子组件,实际上拿到的就是 XML 布局中的根组件。那现在使用 Compose 没有 XML 了,所以就用被设置到 setContentView() 中的 this —— ComposeView 平替 XML 的根组件了。

首次调用 setContent() 的 existingComposeView 应该是 null,因为还没通过 setContentView() 设置要显示的内容,因此 getChildAt(0) 拿到的是 null。所以继续走后续代码,就会进入 else,创建一个新的 ComposeView 并将其传给 setContentView(),在这之后 existingComposeView 就是传入的 ComposeView 对象,也就不再是 null 了,后续如果发生配置变化(如屏幕旋转),重新创建 Activity 再次进入 setContent() 时,existingComposeView 可以被复用而无需重新创建 ComposeView。

接下来关注 if 和 else 中都调用了的两个函数 —— ComposeView 的 setParentCompositionContext() 和 setContent()。前者的作用是设置父组合上下文(用于管理 Composition 的作用域和生命周期),后者的作用是更新 Compose UI 内容,触发重组。

setParentCompositionContext() 是 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():

    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() 流程走完了,并没有看出如何显示 Compose 的组件内容的。这是因为 ComposeView 作为一个 ViewGroup 的子类,也就是 View 的子类,它是在附加到窗口,也就是在重写 onAttachedToWindow() 的逻辑时,才需要显示 UI 内容。

    // AbstractComposeView 直接继承 ViewGroup 并重写了 onAttachedToWindow()override fun onAttachedToWindow() {super.onAttachedToWindow()// 记录与当前页面绑定的 WindowpreviousAttachedWindowToken = windowTokenif (shouldCreateCompositionOnAttachedToWindow) {ensureCompositionCreated()}}

由于 shouldCreateCompositionOnAttachedToWindow 在 onCreate() 内调用 setContent() 的流程中,执行到 ComposeView 的 setContent() 时已经被设为 true 了,所以这里可以进入 if 执行 ensureCompositionCreated():

    private fun ensureCompositionCreated() {// 还没开始组合过程,也就不会有组合结果,再到当前流程中 composition 为 nullif (composition == null) {try {// 标记,是否正在创建组合creatingComposition = true// 进行组合过程并返回组合结果composition = setContent(resolveParentCompositionContext()) {Content()}} finally {// 创建完组合,清除标记creatingComposition = false}}}

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

internal fun AbstractComposeView.setContent(parent: CompositionContext,content: @Composable () -> Unit
): Composition {GlobalSnapshotManager.ensureStarted()val composeView =if (childCount > 0) {getChildAt(0) as? AndroidComposeView} else {removeAllViews(); null} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }return doSetContent(composeView, parent, content)
}

我们先看 ensureCompositionCreated() 给 setContent() 传的实参 resolveParentCompositionContext():

    private fun resolveParentCompositionContext() = parentContext?: findViewTreeCompositionContext()?.cacheIfAlive()?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive }?: windowRecomposer.cacheIfAlive()

parentContext 在看 ComponentActivity.setContent() 的流程时出现过,在 setParentCompositionContext() 内被赋值,是 null。

再看 findViewTreeCompositionContext():

/*** 返回此视图层级结构中当前节点的父组合上下文(parent CompositionContext),如果未找到则返回 null。* 如需获取或设置特定视图的父组合上下文,请参阅 compositionContext。*/
fun View.findViewTreeCompositionContext(): CompositionContext? {var found: CompositionContext? = compositionContextif (found != null) return foundvar parent: ViewParent? = parent// 一直向上找父组件的 CompositionContextwhile (found == null && parent is View) {found = parent.compositionContextparent = parent.getParent()}return found
}

那这个函数的返回值到底如何呢?取决于被查找的 View 的 compositionContext 属性:

/*** The [CompositionContext] that should be used as a parent for compositions at or below* this view in the hierarchy. Set to non-`null` to provide a [CompositionContext]* for compositions created by child views, or `null` to fall back to any [CompositionContext]* provided by ancestor views.**/
var View.compositionContext: CompositionContext?get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContextset(value) {setTag(R.id.androidx_compose_ui_view_composition_context, value)}

在没有给 View 设置 id 为 androidx_compose_ui_view_composition_context 的 Tag 的情况下,compositionContext 属性为 null。通常情况下,我们都不会设置该 id 的 Tag,所以一般为 null。

继续向后,假如 findViewTreeCompositionContext() 找到了 CompositionContext,那么就执行 cacheIfAlive():

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

接下来就是去取缓存 cachedViewTreeCompositionContext,在这个缓存的 CompositionContext 处于活跃状态时返回它。

最后检查 windowRecomposer 并在它处于活跃状态时缓存它,它也是 View 的扩展属性:

/*** Get or lazily create a [Recomposer] for this view's window. The view must be attached* to a window with the [LifecycleOwner] returned by [findViewTreeLifecycleOwner] registered at* the root to access this property.*/
@OptIn(InternalComposeUiApi::class)
internal val View.windowRecomposer: Recomposerget() {check(isAttachedToWindow) {"Cannot locate windowRecomposer; View $this is not attached to a window"}val rootView = contentChildreturn when (val rootParentRef = rootView.compositionContext) {null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)is Recomposer -> rootParentRefelse -> error("root viewTreeParentCompositionContext is not a Recomposer")}}

先看 contentChild 是什么:

/*** Find the "content child" for this view. The content child is the view that is either* a direct child of the view with id [android.R.id.content] (and was therefore set as a* content view into an activity or dialog window) or the root view of the window.** This is used as opposed to [View.getRootView] as the Android framework can reuse an activity* window's decor views across activity recreation events. Since a window recomposer is associated* with the lifecycle of the host activity, we want that recomposer to shut down and create a new* one for the new activity instance.*/
private val View.contentChild: Viewget() {var self: View = thisvar parent: ViewParent? = self.parentwhile (parent is View) {if (parent.id == android.R.id.content) return selfself = parentparent = self.parent}return self}

就是 android.R.id.content 那个 ViewGroup 的子组件,也就是我们在 ComponentActivity.setContent() 中看到的那个 ComposeView —— existingComposeView。

将 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}
}

很明显,这个函数是创建了一个 CompositionContext 对象 newRecomposer,并在返回它之前赋值给 rootView 的 compositionContext 属性。到这里,rootView 的 compositionContext 终于不为空了。

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

/*** A factory for creating an Android window-scoped Recomposer. See createRecomposer.*/
@InternalComposeUiApi
fun interface WindowRecomposerFactory {/*** Get a [Recomposer] for the window where [windowRootView] is at the root of the window's* [View] hierarchy. The factory is responsible for establishing a policy for* [shutting down][Recomposer.cancel] the returned [Recomposer]. [windowRootView] will* hold a hard reference to the returned [Recomposer] until it [joins][Recomposer.join]* after shutting down.*/fun createRecomposer(windowRootView: View): Recomposercompanion object {/*** A [WindowRecomposerFactory] that creates **lifecycle-aware** [Recomposer]s.** Returned [Recomposer]s will be bound to the* [LifecycleOwner] returned by [findViewTreeLifecycleOwner] registered* at the [root][View.getRootView] of the view hierarchy and run* [recomposition][Recomposer.runRecomposeAndApplyChanges] and composition effects on the* [AndroidUiDispatcher.CurrentThread] for the window's UI thread. The associated* [MonotonicFrameClock] will only produce frames when the [Lifecycle] is at least* [Lifecycle.State.STARTED], causing animations and other uses of [MonotonicFrameClock]* APIs to suspend until a **visible** frame will be produced.*/@OptIn(ExperimentalComposeUiApi::class)val LifecycleAware: WindowRecomposerFactory = WindowRecomposerFactory { rootView ->rootView.createLifecycleAwareWindowRecomposer()}}
}

此时就要看 factory.get() 拿到的是什么对象,通过 factory 的定义可以确定 get() 得到的是一个 LifecycleAware,createLifecycleAwareWindowRecomposer() 会返回一个已经准备好(界面刷新)的工具 Recomposer(代码很复杂,这里不贴)。

所以,resolveParentCompositionContext() 最终返回的是一个用于等待和准备刷新的工具 Recomposer。

再看 setContent() 的第二个参数,传的是 AbstractComposeView 的抽象函数 Content():

    @Composable@UiComposableabstract fun Content()

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

    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)@Composableoverride fun Content() {content.value?.invoke()}

把 Content() 放入 lambda 表达式中作为 setContent() 的第二个参数,何时执行就要看该函数的具体内容了:

/*** Composes the given composable into the given view.** The new composition can be logically "linked" to an existing one, by providing a* [parent]. This will ensure that invalidations and CompositionLocals will flow through* the two compositions as if they were not separate.** Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to* be able to save and restore the values used within the composition. See [View.setId].** @param parent The [Recomposer] or parent composition reference.* @param content Composable that will be the content of the view.*/
internal fun AbstractComposeView.setContent(parent: CompositionContext,content: @Composable () -> Unit
): Composition {GlobalSnapshotManager.ensureStarted()val composeView =if (childCount > 0) {getChildAt(0) as? AndroidComposeView} else {removeAllViews(); null} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }return doSetContent(composeView, parent, content)
}

GlobalSnapshotManager.ensureStarted():

/*** Platform-specific mechanism for starting a monitor of global snapshot state writes* in order to schedule the periodic dispatch of snapshot apply notifications.* This process should remain platform-specific; it is tied to the threading and update model of* a particular platform and framework target.** Composition bootstrapping mechanisms for a particular platform/framework should call* [ensureStarted] during setup to initialize periodic global snapshot notifications.* For Android, these notifications are always sent on [AndroidUiDispatcher.Main]. Other platforms* may establish different policies for these notifications.*/
internal object GlobalSnapshotManager {private val started = AtomicBoolean(false)fun ensureStarted() {if (started.compareAndSet(false, true)) {val channel = Channel<Unit>(Channel.CONFLATED)CoroutineScope(AndroidUiDispatcher.Main).launch {// 订阅通知channel.consumeEach {// 通知回调,收到通知后的行为Snapshot.sendApplyNotifications()}}Snapshot.registerGlobalWriteObserver {// 发送通知channel.trySend(Unit)}}}
}

通知回调的行为是去执行 Snapshot.sendApplyNotifications(),主要作用是通知更新,这是一个很复杂的过程。Snapshot 系统是用来管理 Compose 组件依赖的变量的,它会在这些变量发生变化时自动感知到这些变量的变化,并把这些变化应用到界面中,保证使用这些变量最新的值去及时的刷新界面。此外,Snapshot 的复杂还因为它支持了多线程,可以在多个线程中去更新 Compose 组件依赖的变量,而原生只能在主线程中更新 UI。

sendApplyNotifications() 只说关键代码:

sealed class Snapshot {companion object {fun sendApplyNotifications() {val changes = sync {currentGlobalSnapshot.get().modified?.isNotEmpty() == true}if (changes)advanceGlobalSnapshot()}}
}

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

private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {var previousGlobalSnapshot = snapshotInitializer as GlobalSnapshotval result = sync {previousGlobalSnapshot = currentGlobalSnapshot.get()takeNewGlobalSnapshot(previousGlobalSnapshot, block)}// If the previous global snapshot had any modified states then notify the registered apply// observers.val modified = previousGlobalSnapshot.modifiedif (modified != null) {val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }// 把界面中用到的发生变化的变量进行新值的应用,确切地说,把界面中使用到的发生变量变化的部分标记为失效,这样// 失效的部分就会发生重组,然后再发生布局和绘制,完整整个页面的刷新。只标记发生变化的部分是为了节省性能,只刷新// 应该刷新的地方保证耗时最短,刷新效率最高observers.fastForEach { observer ->observer(modified, previousGlobalSnapshot)}}sync {modified?.forEach(::overwriteUnusedRecordsLocked)}return result
}

applyObservers 是在 被添加的:

        // 还是在 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)}}}

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

    private suspend fun recompositionRunner(block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit) {val parentFrameClock = coroutineContext.monotonicFrameClockwithContext(broadcastFrameClock) {// Enforce mutual exclusion of callers; register self as current runnerval callingJob = coroutineContext.jobregisterRunnerJob(callingJob)// Observe snapshot changes and propagate them to known composers only from// this caller's dispatcher, never working with the same composer in parallel.// unregisterApplyObserver is called as part of the big finally below// 注册回调,收到回调之后,通过 resume() 开始执行重组工作val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->synchronized(stateLock) {if (_state.value >= State.Idle) {snapshotInvalidations.addAll(changed)deriveStateLocked()} else null}?.resume(Unit)}addRunning(recomposerInfo)try {// Invalidate all registered composers when we start since we weren't observing// snapshot changes on their behalf. Assume anything could have changed.synchronized(stateLock) {knownCompositions.fastForEach { it.invalidateAll() }// Don't need to deriveStateLocked here; invalidate will do it if needed.}coroutineScope {block(parentFrameClock)}} finally {unregisterApplyObserver.dispose()synchronized(stateLock) {if (runnerJob === callingJob) {runnerJob = null}deriveStateLocked()}removeRunning(recomposerInfo)}}}

回到 sendApplyNotifications(),该函数就是触发重组的。然后再往下看 registerGlobalWriteObserver(),该函数是对变量写行为的监听,当变量有变化时就会执行一次后面的 lambda,通过 channel 发送一次通知。每次发通知,都会让接收端执行 consumeEach() 后的 lambda,也就是触发重组。这里利用 Channel 的特性对重组触发次数做了优化,一帧中只会触发一或两次重组,从而避免导致高频修改引发高频刷新的性能问题。

再看 AbstractComposeView 的第二部分,生成一个 AndroidComposeView 并将其添加到视图中,形成一个 ComposeView 包含 AndroidComposeView 的结构。实际上,AndroidComposeView 才是负责显示与触摸反馈(真正干活的)的那个 View。

最后一部分是 doSetContent():

private fun doSetContent(owner: AndroidComposeView,parent: CompositionContext,content: @Composable () -> Unit
): Composition {...// 创建一个 Composition 对象,实际类型为 CompositionImplval original = Composition(UiApplier(owner.root), parent)// 先通过 Tag 去拿 WrappedComposition,如果拿到的为空则自行创建一个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
}

调用 WrappedComposition 的 setContent():

    override fun setContent(content: @Composable () -> Unit) {// 约等于对 onAttachedToWindow() 的监听owner.setOnViewTreeOwnersAvailable {if (!disposed) {val lifecycle = it.lifecycleOwner.lifecyclelastContent = content// 第一次进入 if,第二次进入 else ifif (addedToLifecycle == null) {addedToLifecycle = lifecycle// this will call ON_CREATE synchronously if we already createdlifecycle.addObserver(this)} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {// original 是 CompositionImploriginal.setContent {@Suppress("UNCHECKED_CAST")val inspectionTable =owner.getTag(R.id.inspection_slot_table_set) as?MutableSet<CompositionData>?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)as? MutableSet<CompositionData>if (inspectionTable != null) {inspectionTable.add(currentComposer.compositionData)currentComposer.collectParameterInformation()}LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {ProvideAndroidCompositionLocals(owner, content)}}}}}}

看 CompositionImpl 的 setContent():

    override fun setContent(content: @Composable () -> Unit) {check(!disposed) { "The composition is disposed" }// 又把最外面的 setContent() 后的 lambda 保存了一次this.composable = content// parent 类型是 CompositionContext,实际上是前面的 Recomposer,用于刷新的,不是用于初始化的,在 doSetContent()// 内创建 Composition 时作为第二个参数传入parent.composeInitial(this, composable)}

既然 parent 是 Recomposer,那么自然就要去看 Recomposer 的 composeInitial():

    internal override fun composeInitial(composition: ControlledComposition,content: @Composable () -> Unit) {val composerWasComposing = composition.isComposingtry {composing(composition, null) {composition.composeContent(content)}} catch (e: Exception) {processCompositionError(e, composition, recoverable = true)return}// TODO(b/143755743)if (!composerWasComposing) {Snapshot.notifyObjectsInitialized()}synchronized(stateLock) {if (_state.value > State.ShuttingDown) {if (composition !in knownCompositions) {knownCompositions += composition}}}try {performInitialMovableContentInserts(composition)} catch (e: Exception) {processCompositionError(e, composition, recoverable = true)return}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") {snapshot = currentSnapshot()compositionToken = snapshot.idproviderUpdates.clear()invalidationsRequested.forEach { scope, set ->val location = scope.anchor?.location ?: returninvalidations.add(Invalidation(scope, location, set))}invalidations.sortBy { it.location }nodeIndex = 0var complete = falseisComposing = truetry {startRoot()// vv Experimental for forced@Suppress("UNCHECKED_CAST")val savedContent = nextSlot()if (savedContent !== content && content != null) {updateValue(content as Any?)}// ^^ Experimental for forced// Ignore reads of derivedStateOf recalculationsobserveDerivedStateRecalculations(start = {childrenComposing++},done = {childrenComposing--},) {if (content != null) {startGroup(invocationKey, invocation)// 关键代码invokeComposable(this, content)endGroup()} else if ((forciblyRecompose || providersInvalid) &&savedContent != null &&savedContent != Composer.Empty) {startGroup(invocationKey, invocation)@Suppress("UNCHECKED_CAST")invokeComposable(this, savedContent as @Composable () -> Unit)endGroup()} else {skipCurrentGroup()}}endRoot()complete = true} finally {isComposing = falseinvalidations.clear()if (!complete) abortRoot()}}}

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 参数就是 setContent() 的尾随 lambda 指定的 Compose 组件内容,它被强转为 Function2,是因为 Compose 编译器插件会对所有 @Composable 函数添加两个参数。最后调用转换成 Function2 的 realFn(),也就是执行 lambda 内的内容了。

以上就是 setContent() 的内容,它并没有对界面的显示做任何工作,它所做的就是布置好各种监听器,以便变量发生变化时触发界面刷新。真正负责显示的是各个组件 Composable 函数内的通用函数 Layout():

@UiComposable
@Composable inline fun Layout(content: @Composable @UiComposable () -> Unit,modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
) {val density = LocalDensity.currentval layoutDirection = LocalLayoutDirection.currentval viewConfiguration = LocalViewConfiguration.currentReusableComposeNode<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)
}
@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()
}
    // 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)}}

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

相关文章:

Compose 原理解析

Compose 的组件都是放在 setContent() 之后才能显示的&#xff0c;那需要先看看这个函数的作用。 先看 ComponentActivity 的扩展函数 setContent()&#xff1a; /*** 将给定的可组合项合成到给定的 Activity 中。[content] 将成为给定 Activity 的根视图。* 这大致相当于使用…...

pyspark学习rdd处理数据方法——学习记录

python黑马程序员 """ 文件&#xff0c;按JSON字符串存储 1. 城市按销售额排名 2. 全部城市有哪些商品类别在售卖 3. 上海市有哪些商品类别在售卖 """ from pyspark import SparkConf, SparkContext import os import jsonos.environ[PYSPARK_P…...

个人学习编程(3-22) leetcode刷题

连续子数组&#xff1a;&#xff08;难&#xff09; 示例 1: 输入: nums [0,1] 输出: 2 说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。 示例 2: 输入: nums [0,1,0] 输出: 2 说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。 需要理解的知识&a…...

RabbitMQ八股文

RabbitMQ 核心概念与组件 1. RabbitMQ 核心组件及其作用 1.1 生产者&#xff08;Producer&#xff09; 作用&#xff1a;创建并发送消息到交换机。特点&#xff1a;不直接将消息发送到队列&#xff0c;而是通过交换机路由。 1.2 交换机&#xff08;Exchange&#xff09; 作…...

运维面试题(七)

1.statefulset用来管理有状态的应用程序&#xff0c;有状态是什么意思&#xff1f; 每一个pod都有一个固定的网络标识符&#xff0c;在整个生命周期中不会改变。每个实例都可以拥有自己的持久化存储卷&#xff0c;即使容器被删除并重新创建&#xff0c;存储卷仍然存在。Statef…...

【项目设计】网页版五子棋

文章目录 一、项目介绍1.项目简介2.开发环境3.核心技术4.开发阶段 二、Centos-7.6环境搭建1.安装wget工具2.更换软件源(yum源)3.安装scl工具4.安装epel软件源5.安装lrzsz传输工具6.安装高版本gcc/g编译器7.安装gdb调试器8.安装git9.安装cmake10.安装boost库11.安装Jsoncpp库12.…...

Netty——BIO、NIO 与 Netty

文章目录 1. 介绍1.1 BIO1.1.1 概念1.1.2 工作原理1.1.3 优缺点 1.2 NIO1.2.1 概念1.2.2 工作原理1.2.3 优缺点 1.3 Netty1.3.1 概念1.3.2 工作原理1.3.3 优点 2. Netty 与 Java NIO 的区别2.1 抽象层次2.2 API 易用性2.3 性能优化2.4 功能扩展性2.5 线程模型2.6 适用场景 3. 总…...

Docker 安装 Mysql

以下是安装Docker版MySQL 8.0.25并实现目录挂载的步骤&#xff1a; docker仓库&#xff1a;https://hub.docker.com/_/mysql 1. 拉取Mysql镜像文件 docker pull mysql:8.0.252. 创建mysql临时容器服务 docker run -d \--name mysql \-p 3306:3306 \-e MYSQL_ROOT_PASSWORD123…...

Electron打包文件生成.exe文件打开即可使用

1 、Electron 打包&#xff0c;包括需要下载的内容和环境配置步骤 注意&#xff1a;Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的框架 首先需要电脑环境有Node.js 和 npm我之前的文章有关nvm下载node的说明也可以去官网下载 检查是否有node和npm环…...

线程和协程的区别了解

1.资源消耗 调度方式&#xff1a;线程由操作系统内核调度&#xff08;抢占式&#xff09;&#xff0c;协程由程序自己控制调度&#xff08;协作式&#xff09;。切换开销&#xff1a;线程切换涉及内核态与用户态的转换&#xff0c;开销大&#xff1b;协程只在用户态切换上下文…...

楼宇自控系统的结构密码:总线与分布式结构方式的差异与应用

在现代建筑中&#xff0c;为了实现高效、智能的管理&#xff0c;楼宇自控系统变得越来越重要。它就像建筑的 智能管家&#xff0c;可自动控制照明、空调、通风等各种机电设备&#xff0c;让建筑运行更顺畅&#xff0c;还能节省能源成本。而在楼宇自控系统里&#xff0c;有两种关…...

算法及数据结构系列 - 滑动窗口

系列文章目录 算法及数据结构系列 - 二分查找 算法及数据结构系列 - BFS算法 算法及数据结构系列 - 动态规划 算法及数据结构系列 - 双指针 算法及数据结构系列 - 回溯算法 算法及数据结构系列 - 树 文章目录 滑动窗口框架思路经典题型76. 最小覆盖子串567. 字符串的排列438. …...

【江协科技STM32】软件SPI读写W25Q64芯片(学习笔记)

SPI通信协议及S为5Q64简介&#xff1a;【STM32】SPI通信协议&W25Q64Flash存储器芯片&#xff08;学习笔记&#xff09;-CSDN博客 STM32与W25Q64模块接线&#xff1a; SPI初始化&#xff1a; 片选SS、始终SCK、MOSI都是主机输出引脚&#xff0c;输出引脚配置为推挽输出&…...

2025.3.23机器学习笔记:文献阅读

2025.3.23周报 题目信息摘要Abstract创新点网络架构实验不足以及展望 题目信息 题目&#xff1a; Enhancement of Hydrological Time Series Prediction with Real-World Time Series Generative Adversarial Network-Based Synthetic Data and Deep Learning Models期刊&…...

Day20-前端Web案例——部门管理

目录 部门管理1. 前后端分离开发2. 准备工作2.1 创建Vue项目2.2 安装依赖2.3 精简项目 3. 页面布局3.1 介绍3.2 整体布局3.3 左侧菜单 4. Vue Router4.1 介绍4.2 入门4.3 案例4.4 首页制作 5. 部门管理5.1部门列表5.1.1. 基本布局5.1.2 加载数据5.1.3 程序优化 5.2 新增部门5.3…...

实验3 以太坊交易周期的需求分析

区块链技术 实验报告 实验名称 实验3 以太坊交易周期的需求分析 一、实验目的 1、学习并掌握以太坊交易的内容&#xff1b; 2、学习并掌握以太坊交易周期的四个阶段&#xff1b; 3、学习并掌握结构化需求分析方法&#xff1b; 4、学习并掌握面向对象的需求分析方法&…...

Linux 通过压缩包安装 MySQL 并设置远程连接教程

一、引言 在 Linux 系统中,有时候我们需要通过压缩包的方式手动安装 MySQL 数据库,并且为了方便在其他设备上对数据库进行管理和操作,还需要设置允许远程连接。本文将详细介绍在 Linux(以 CentOS 为例)系统中通过压缩包安装 MySQL 8 并设置远程连接的步骤。 二、安装前准…...

【商城实战(56)】商城数据生命线:恢复流程与演练全解析

【商城实战】专栏重磅来袭&#xff01;这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建&#xff0c;运用 uniapp、Element Plus、SpringBoot 搭建商城框架&#xff0c;到用户、商品、订单等核心模块开发&#xff0c;再到性能优化、安全加固、多端适配&#xf…...

Java学习笔记-XXH3哈希算法

XXH3是由Yann Collet设计的非加密哈希算法&#xff0c;属于XXHash系列的最新变种&#xff0c;专注于极速性能与低碰撞率&#xff0c;适用于对计算效率要求极高的场景。 极速性能 在RAM速度限制下运行&#xff0c;小数据&#xff08;如 1-128 字节&#xff09;处理可达纳秒级&…...

同旺科技USB to SPI 适配器 ---- 指令循环发送功能

所需设备&#xff1a; 内附链接 1、同旺科技USB to SPI 适配器 1、周期性的指令一次输入&#xff0c;即可以使用 “单次发送” 功能&#xff0c;也可以使用 “循环发送” 功能&#xff0c;大大减轻发送指令的编辑效率&#xff1b; 2、 “单次发送” 功能&#xff0c;“发送数据…...

在Mac M1/M2芯片上完美安装DeepCTR库:避坑指南与实战验证

让推荐算法在Apple Silicon上全速运行 概述 作为推荐系统领域的最经常用的明星库&#xff0c;DeepCTR集成了CTR预估、多任务学习等前沿模型实现。但在Apple Silicon架构的Mac设备上&#xff0c;安装过程常因ARM架构适配、依赖库版本冲突等问题受阻。本文通过20次环境搭建实测…...

【CXX-Qt】2.5 继承

某些 Qt API 要求你从抽象基类中重写某些方法&#xff0c;例如 QAbstractItemModel。 为了支持直接从 Rust 中创建这样的子类&#xff0c;CXX-Qt 提供了多种辅助工具。 某些基类可能需要特殊的构造参数。这可以通过使用自定义构造函数来实现。 访问基类方法 要在 Rust 中访…...

Linux系统之美:环境变量的概念以及基本操作

本节重点 理解环境变量的基本概念学会在指令和代码操作上查询更改环境变量环境变量表的基本概念父子进程间环境变量的继承与隔离 一、引入 1.1 自定义命令&#xff08;我们的exe&#xff09; 我们以往的Linux编程经验告诉我们&#xff0c;我们在对一段代码编译形成可执行文件后…...

【nnUnetv2】推理+评估+测试

在 Windows 系统下设置环境变量 之前训练和推理的时候开着AutoDL的服务器,是在 Linux 系统下设置的环境变量。 但是现在开始研究具体代码了,就在本地跑(一直开着服务器有点费钱),所以就在Windows 系统下设置环境变量。 ①右键点击 “此电脑”,选择 “属性”。 ②在左侧…...

损失函数理解(一)——极大似然估计

本博客内容来自B站up主【王木头学科学】的视频内容 习惯看视频的小伙伴可移至视频链接[待补充]&#xff1a;~~~ 首先通俗地解释一下极大似然估计&#xff08;Maximum Likelihood Estimation&#xff0c;MLE&#xff09;的思想&#xff1a;通过结果寻找使该结果发生的最可能的原…...

ios端使用TCplayer直播播放三秒直接卡顿bug

1. 查看配置项没问题 setTcPlayer() {let that this;player new TcPlayer("videoPlayer", {live: this.activatPlayType "livePlay" ? true : false,x5_type: "h5",x5_fullscreen: true,systemFullscreen: true,x5_orientation: 1,x5_player…...

大模型-提示词工程与架构

什么是提示工程 提示工程&#xff08;Prompt Engineering&#xff09;是一门新兴的技术领域&#xff0c;专注于研究如何设计、构建和优化提示词&#xff0c;以充分发挥大模型的潜力 。它涉及到对语言结构、任务需求、模型特性等多方面因素的综合考量。提示工程的目标是通过精心…...

高斯数据库-WDR Snapshot生成性能报告

docker 安装高斯数据库&#xff1a; docker pull opengauss/opengauss:latestdocker run --name opengauss --privilegedtrue -d -e GS_PASSWORDopenGauss123 -p 8090:5432 -v /opengauss:/var/lib/opengauss/data opengauss/opengauss:latest 进入容器设置用户权限&#xff…...

损失函数理解(二)——交叉熵损失

损失函数的目的是为了定量描述不同模型&#xff08;例如神经网络模型和人脑模型&#xff09;的差异。 交叉熵&#xff0c;顾名思义&#xff0c;与熵有关&#xff0c;先把模型换成熵这么一个数值&#xff0c;然后用这个数值比较不同模型之间的差异。 为什么要做这一步转换&…...

CSS学习笔记

【1】CSS样式规则 【2】CSS样式表引入方式 1、行内式 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"/><meta name"vi…...

AI比人脑更强,因为被植入思维模型【15】马斯洛需求层次理论

马斯洛需求层次模型思维模型 定义 马斯洛需求层次模型是由美国心理学家亚伯拉罕马斯洛&#xff08;Abraham Maslow&#xff09;于1943年提出的一种心理学理论&#xff0c;用于描述人类动机的层次结构。该模型将人类的需求从低到高分为五个层次&#xff0c;分别是生理需求、安…...

cartographer中地图转换

文章目录 地图种类栅格地图 坐标系种类ros坐标系像素坐标系物理坐标系(世界坐标系) 地图种类 栅格地图 地图的初始化 在Cartographer中&#xff0c;栅格地图通过概率值来表示每个栅格的状态。每个栅格的初始概率值通常设置为0.5&#xff0c;表示未知状态。这种初始化方式允许…...

关于MTU的使用(TCP/IP网络下载慢可能与此有关)

参考链接&#xff1a;告诉你mtu值怎么设置才能网速最好&#xff01; -Win7系统之家 出现网络速度被限制&#xff0c;可能与MTU值相关&#xff0c;先查看下本机的MTU winR,然后输入&#xff1a;netsh interface ipv4 show subinterfaces &#xff0c;查看自己网络中的MTU&…...

【AI解题】Cache直接映射地址划分解析

一、问题背景 某32位总线处理器的Cache采用直接映射方式&#xff0c;已知 Cache总容量为16KB&#xff0c;每个Cache块大小为16字节。需要确定内存地址中 Offset&#xff08;块内偏移&#xff09;、Index&#xff08;块索引&#xff09;、Tag&#xff08;标签&#xff09; 三部…...

android音频概念解析

音频硬件接口&#xff08;我们可以理解为ASOC的声卡&#xff09; 官方代码里叫audio hardware interface 也称为module&#xff0c;定义在services/audiopolicy/config/audio_policy_configuration.xml&#xff1a; 分别有primary&#xff0c;a2dp&#xff0c;usb&#xff0…...

项目生命周期 和 项目管理生命周期的差异

在项目管理中,明确区分 项目生命周期 和 项目管理生命周期 是理解项目运作的关键。以下从定义、阶段划分到实际应用进行系统性分析: 一、项目生命周期(Project Life Cycle) 定义 项目生命周期是项目从 启动到结束 的自然演进过程,描述项目交付成果的 技术性阶段,通常与…...

UDP 协议

文章目录 UDP 协议简介数据包格式UDP 通信流程抓包分析参考 本文为笔者学习以太网对网上资料归纳整理所做的笔记&#xff0c;文末均附有参考链接&#xff0c;如侵权&#xff0c;请联系删除。 UDP 协议 UDP 是一种面向无连接的传输层协议&#xff0c;属于 TCP/IP 协议簇的一种。…...

[已解决]jupyter notebook报错 500 : Internal Server Error及notebook闪退

jupyter notebook出现如上图的报错&#xff0c;可以在黑色窗口中检查是为什么报错。 我检查发现是nbconvert导致的问题&#xff0c;卸载重装nbconvert。 但是这时候出现&#xff0c;jupyter notebook闪退问题。jupyter的黑色窗口出现一秒钟就没了。 在Anaconda Prompt中检查ju…...

APM 仿真遥控指南

地面站开发了一段时间了&#xff0c;由于没有硬件&#xff0c;所以一直在 APM 模拟器中验证。我们已经实现了 MAVLink 消息接收和解析&#xff0c;显示无人机状态&#xff0c;给无人机发送消息&#xff0c;实现一键起飞&#xff0c;飞往指定地点&#xff0c;降落&#xff0c;返…...

使用 ncurses 库创建文本用户界面:基础函数详解

简介 ncurses 是一个功能强大的库&#xff0c;用于在 Unix-like 系统中创建文本用户界面。它提供了丰富的函数来控制屏幕上的文本显示、处理键盘输入、绘制图形元素等。本文将详细介绍 ncurses 库中的一些基础函数&#xff0c;包括 printw、wrefresh、获取用户信息、键盘输入、…...

dify创建第一个Agent

1、首先LLM模型必须支持 Function Calling 由于deepseek-R1本地化部署时还不支持&#xff0c;所以使用 qwq模型。 2、创建空白 Agent 3、为Agent添加工具 4、测试 当未添加时间工具时 询问 时间 如下 5、开启时间工具 询问如下...

nebula graph传统使用Docker进行项目发版

nebula graph传统使用Docker进行项目发版 1. nebula graph服务2. 搭建ES集群3. 注意事项3.1 图数据库的启动顺序3.2 模糊查询失效 1. nebula graph服务 1.在测试服务器中执行如下命令 docker commit 85b6e2b8xxx xxx_nebula_es:1.0.0.2执行docker images之后能看到新的镜像 x…...

OpenCV vs MediaPipe:哪种方案更适合实时手势识别?

引言 手势识别是计算机视觉的重要应用&#xff0c;在人机交互&#xff08;HCI&#xff09;、增强现实&#xff08;AR&#xff09;、虚拟现实&#xff08;VR&#xff09;、智能家居控制、游戏等领域有广泛的应用。实现实时手势识别的技术方案主要有基于传统计算机视觉的方法&am…...

PRODIGY: “不折腾人”的蛋白-蛋白/蛋白-小分子结合能计算工具

PRODIGY&#xff08;全称为 PROtein binDIng enerGY prediction&#xff09;是一种蛋白质结合能预测工具&#xff0c;可利用蛋白质-蛋白质复合物的三维结构来预测其结合亲和力。PRODIGY 利用一种高效的基于接触的方法&#xff0c;在估计结合自由能和解离常数的同时&#xff0c;…...

IDEA修改默认作者名称

User: IDEA提示注释缺少author信息&#xff0c;但自动设置后&#xff0c;名称不是我想要的默认名称&#xff0c;应该如何修改IDEA里默认的作者名称&#xff1f; Kimi: 以下是几种修改IntelliJ IDEA中默认作者名称的方法&#xff1a; ### 方法一&#xff1a;修改File and Code …...

【嵌入式学习2】C语言 - VScode环境搭建

目录 ## 语言分类 ## c语言编译器 ## VScode相关配置 ## 语言分类 编译型语言&#xff1a;C&#xff0c;C解释型语言&#xff1a;python&#xff0c;JS ## c语言编译器 分类GCC 系列MinGWCygwinMSVC系列一套编程语言编译器将GCC编译器和GNU Binutils移植到Win32平台下的产物…...

【TI MSPM0】Timer学习

一、计数器 加法计数器&#xff1a;每进入一个脉冲&#xff0c;就加一减法计算器&#xff1a;每进入一个脉冲&#xff0c;就减一 当计数器减到0&#xff0c;触发中断 1.最短计时时间 当时钟周期为1khz时&#xff0c;最短计时时间为1ms&#xff0c;最长计时时间为65535ms 当时…...

SQL Server数据库慢SQL调优

SQL Server中慢SQL会显著降低系统性能并引发级联效应。首先&#xff0c;用户直接体验响应时间延长&#xff0c;核心业务操作&#xff08;如交易处理、报表生成&#xff09;效率下降&#xff0c;导致客户满意度降低甚至业务中断。其次&#xff0c;资源利用率失衡&#xff0c;CPU…...

大数据平台上的数据建模与分析:从数据到决策的跃迁

大数据平台上的数据建模与分析:从数据到决策的跃迁 随着数字化转型的深入,大数据平台成为了企业实现智能决策和创新的核心技术基础。大量结构化、半结构化和非结构化数据的生成和存储,促使企业需要更高效的方式来管理、分析、以及从中提取有价值的信息。在这一过程中,数据…...

C++ --- 多态

1 多态的概念 多态(polymorphism)的概念&#xff1a;通俗来说&#xff0c;就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态)&#xff0c;这⾥我们重点讲运⾏时多态&#xff0c;编译时多态(静态多态)和运⾏时多态(动态多态)。编译时 多态(静态多态)主要就是我…...