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 之間是使用鏈表將它們連接起來的。使用它的目的是為了提升性能。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/73028.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/73028.shtml
英文地址,請注明出處:http://en.pswp.cn/web/73028.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

細說衛星導航:測距定位原理

測距定位原理 1. 偽距測量技術 核心原理&#xff1a;衛星發射信號&#xff0c;用戶接收并記錄傳播時間&#xff0c;乘以光速得到距離&#xff08;偽距&#xff09;。 技術細節&#xff1a; 信號傳播路徑分析 信號結構&#xff1a; 衛星信號包含三部分&#xff1a; 載波&…

19921 多重背包

19921 多重背包 ??難度&#xff1a;中等 &#x1f31f;考點&#xff1a;動態規劃、背包問題 &#x1f4d6; &#x1f4da; import java.util.Arrays; import java.util.LinkedList; import java.util.Queue; import java.util.Scanner;public class Main {static int N …

js逆向之斷點調試

1.XHR/提取斷點用法 當刷新頁面時候&#xff0c;有大量請求&#xff0c;并且你無法定位參數信息的時候&#xff0c;或者參數被混淆無法搜到&#xff0c;可以用該方法&#xff0c;該方法是會捕獲所有請求連接&#xff0c;然后我們通過連接過濾出自己想要的請求&#xff0c;然后…

基于32單片機的無人機直流電機閉環調速系統設計

標題:基于32單片機的無人機直流電機閉環調速系統設計 內容:1.摘要 本文針對無人機直流電機調速需求&#xff0c;設計了基于32單片機的無人機直流電機閉環調速系統。背景在于無人機應用場景不斷拓展&#xff0c;對電機調速精度和穩定性要求日益提高。目的是開發一套高精度、響應…

如何用Deepseek制作流程圖?

使用Deepseek制作流程圖&#xff0c;本質上是讓AI根據你的需求&#xff0c;生成相關流程圖的代碼&#xff0c;然后在流程圖編輯器中渲染&#xff0c;類似于Python一樣&#xff0c;ChatGPT可以生成代碼&#xff0c;但仍需在IDE中執行。 你知道繪制流程圖最高效的工具是什么嗎&a…

嵌入式硬件工程師從小白到入門-原理圖(三)

原理圖繪制從小白到入門&#xff1a;知識點速通與注意事項 一、原理圖繪制基礎概念 什么是原理圖&#xff1f; 原理圖&#xff08;Schematic&#xff09;是電子電路的圖形化表示&#xff0c;展示元器件之間的電氣連接關系&#xff0c;是硬件設計的藍圖。 核心元素 元器件符號&…

WSL 環境橋接與雷達通信配置筆記

作者: DWDROME 維護時間: 2025-03-22 參考文章:Windows子系統&#xff08;WSL&#xff09;通過橋接網絡實現被外部局域網主機直接訪問 WSL 環境橋接與雷達通信配置筆記 環境說明 Windows 11 專業版&#xff08;啟用 Hyper-V&#xff09;WSL2 Ubuntu 20.04物理網線&#xff08…

ToDesk云電腦各類鼠標有什么區別?虛擬/3D/游戲鼠標等各有利

不知道各位在使用ToDesk云電腦的時候是否是有注意到&#xff0c;這其中的鼠標竟有多種名稱、多種模式可以選&#xff0c;比如鎖定鼠標、3D鼠標、游戲鼠標這幾項。 那么這些不同名稱的鼠標都代表什么意思吶&#xff0c;又應該怎么選擇、怎么用吶&#xff1f;本篇內容小編就為大…

DeepBI:重構流量邏輯,助力亞馬遜廣告實現高效流量增長

在日益激烈的跨境電商競爭環境中&#xff0c;廣告投放早已從“粗放撒網”走向“精細化運營”。尤其是在亞馬遜這樣一個成熟且競爭白熱化的平臺&#xff0c;如何在廣告預算有限的前提下實現高效曝光、精準觸達、穩定轉化&#xff0c;成為眾多賣家和運營團隊面臨的核心挑戰。 De…

java項目之基于ssm的畢業論文管理系統(源碼+文檔)

項目簡介 畢業論文管理系統實現了以下功能&#xff1a; 本畢業論文管理系統主要實現的功能模塊包括學生模塊、導師模塊和管理員模塊三大部分&#xff0c;具體功能分析如下&#xff1a; &#xff08;1&#xff09;導師功能模塊&#xff1a;導師注冊登錄后主要功能模塊包括個人…

【自學筆記】Linux基礎知識點總覽-持續更新

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 Linux 基礎知識點總覽目錄Linux 簡介文件和目錄結構常用命令文件操作目錄操作權限管理文本處理 Shell 腳本基礎進程管理用戶和組管理網絡配置 總結 Linux 基礎知識點…

【PCB工藝】晶體管的發展歷史

晶體管被認為是20世紀最偉大的發明之一&#xff0c;因為沒有晶體管就不會有現代電腦、手機或平板??&#xff0c;你也無法閱讀到這里的內容&#xff0c;因為不存在網絡。 ——本文純粹出于對過往奮斗在這個領域中科學家的緬懷。科學家有太多寶貴的思想和經驗值得我們認真總結和…

第23章:Kubernetes網絡模型深度剖析

第23章:Kubernetes網絡模型深度剖析 作者:DogDog_Shuai 閱讀時間:約25分鐘 難度:高級 目錄 1. 引言2. Kubernetes網絡模型基礎3. 四種網絡通信模式4. CNI架構深度解析5. 網絡實現原理

HTML應用指南:利用GET請求獲取貓眼電影日票房信息——以哪吒2為例

2025年春節檔期&#xff0c;國產動畫電影《哪吒之魔童鬧海》&#xff08;以下簡稱《哪吒2》&#xff09;以顛覆性的敘事風格與工業化制作水準震撼登場&#xff0c;不僅刷新了中國動畫電影的票房紀錄&#xff0c;更成為全球影史現象級作品。影片憑借春節檔期的爆發式開局、持續5…

Model Context Protocol:下一代AI系統集成范式革命

在2023年全球AI工程化報告中,開發者面臨的核心痛點排名前三的分別是:模型與業務系統集成復雜度(58%)、上下文管理碎片化(42%)、工具調用標準化缺失(37%)。傳統API集成模式在對接大語言模型時暴露明顯短板:RESTful接口無法承載動態上下文,GraphQL缺乏工具編排能力,gR…

Java 鎖機制全面解析

在 Java 并發編程中&#xff0c;鎖&#xff08;Lock&#xff09;是保證線程安全的關鍵工具。本文將全面介紹 Java 的鎖機制&#xff0c;包括 synchronized 關鍵字、Lock 接口及其實現、讀寫鎖、樂觀鎖與悲觀鎖等&#xff0c;幫助新手理解 Java 并發控制。 1. Java 中的鎖概述 …

JavaScript 中 “new Map()”的使用

new Map() 是 JavaScript 中用于創建 Map 對象 的構造函數。Map 是一種鍵值對集合&#xff0c;類似于普通對象&#xff08;Object&#xff09;&#xff0c;但有以下區別&#xff1a; 1. Map 的特點 1.1 鍵的類型 Map&#xff1a;鍵可以是任意類型&#xff08;包括對象、函數、…

Rust語言的集成測試

Rust語言的集成測試 引言 隨著軟件開發的不斷發展&#xff0c;測試已成為一個不可或缺的環節。特別是在系統復雜度日益增加的今天&#xff0c;確保代碼質量和穩定性變得尤為重要。Rust作為一門強調安全性和性能的編程語言&#xff0c;其測試框架提供了豐富的工具來幫助開發者…

手寫簡單的Spring基于注解配置的程序

需求說明&#xff1a; 自己寫一個簡單的 Spring 容器, 通過讀取類的注解(Component ControllerService Reponsitory) &#xff0c;將對象注入到 IOC 容器&#xff0c;自己使用 IOAnnotaion反射集合 技術實現 思路分析&#xff1a; 一、新建一個包component并在包下創建bean類 …

WSL 導入完整系統包教程

作者&#xff1a; DWDROME 配置環境&#xff1a; OS: Ubuntu 20.04.6 LTS on Windows 11 x86_64Kernel: 5.15.167.4-microsoft-standard-WSL2ros-noetic &#x1f9ed;WSL 導入完整系統包教程 ? 一、準備導出文件 假設你已有一個 .tar 的完整系統包&#xff08;如從 WSL 或 L…