扒一扒 Jetpack Compose 實現原理

語言: CN / TW / HK

圖片來自:https://developer.android.google.cn/jetpack/compose

本文作者:goolong

Compose 是 Google 推出的現代化 UI 開發工具包,基於聲明式 UI 開發風格,加上 @Composable 函數幫助開發者有效的實現關注點分離,另外 Compose 內部最大程度優化了重組範圍,可以幫助我們高效的刷新UI,考慮到 Compose 整體架構設計過於複雜,這篇文章主要帶大家瞭解 Compose Runtime 層核心的實現邏輯。

聲明式UI

聲明式 UI 對於 Android 開發同學可能有點陌生,不過熟悉 React 和 Flutter 的同學應該比較清楚,不管是 React、Flutter、Compose,核心都是 MVI 架構方式,通過數據驅動 UI,底層需要維護相應的 UI Tree,比如 React 的 VirtualDOM,Flutter 的 Element,而 Compose 的核心是 Composition。

所謂 "數據驅動UI",就是當 state 變化時,重建這顆樹型結構並基於這棵 NodeTree 刷新 UI。 當然,出於性能考慮,當 NodeTree 需要重建時,各框架會使用 VirtualDom 、GapBuffer(或稱SlotTable) 等不同技術對其進行 "差量" 更新,避免 "全量" 重建。compose.runtime 的重要工作之一就是負責 NodeTree 的創建與更新。

@Composable

@Copmposable 並不是一個註解處理器,Compose 在 Kotlin 編譯器的類型檢測和代碼生成階段依賴 Kotlin 編譯器插件工作,工作原理有點類似於 Kotlin Coroutine 協程的 suspend 函數,suspend 函數在 Kotlin 插件編譯時生成帶有 $continuation 參數(掛起點),而 Compose 函數生成帶有參數 $composer,因此 Compose 也被網友戲稱為 “KotlinUI”

類似於在 suspend 函數中可以調用普通函數和 suspend 函數,而普通函數中不能調用 suspend 函數,Compose 函數也遵循這一規則,正是因為普通函數中不帶有 Kotlin 編譯器生成的 $composer 參數。

```kotlin fun Example(a: () -> Unit, b: @Composable () -> Unit) { a() // 允許 b() // 不允許 }

@Composable fun Example(a: () -> Unit, b: @Composable () -> Unit) { a() // 允許 b() // 允許 } ```

生命週期

所有的 Compose 函數都是一個可組合項,當 Jetpack Compose 首次運行可組合項時,在初始組合期間,它將跟蹤您為了描述組合中的界面而調用的可組合項。當應用的狀態發生變化時,Jetpack Compose 會安排重組,重組是指 Jetpack Compose 重新執行可能因狀態更改而更改的可組合項,然後更新組合以反映所有更改。

參考 Google Jetpack 文檔的例子:

```kotlin @Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition }

@Composable fun LoginInput() { / ... / } ```

Compose NodeTree

前面介紹了 Compose 一些基礎知識,Android 同學都知道 View 體系中構建了一顆 View 樹,而在 Compose 中也是這樣,不過在Compose 中有兩顆樹(類似於 React ),一顆虛擬樹 SlotTable (負責樹構建和重組,類似 React 中的 VirtualDom ),一顆真實的樹 LayoutNode (負責測量和繪製)。

首先我們來看下 Compose UI 中如何構建 Layout 佈局代碼,直接看 setContent 方法。

```kotlin internal fun ViewGroup.setContent( parent: CompositionContext, content: @Composable () -> Unit ): Composition { GlobalSnapshotManager.ensureStarted() // 開啟snapshot監聽(非常重要,後面會講到) val composeView = if (childCount > 0) { getChildAt(0) as? AndroidComposeView } else { removeAllViews(); null } ?: AndroidComposeView(context).also { // 創建AndroidComposeView,並添加到ViewGroup() addView(it.view, DefaultLayoutParams) } return doSetContent(composeView, parent, content) }

@OptIn(InternalComposeApi::class) private fun doSetContent( owner: AndroidComposeView, parent: CompositionContext, content: @Composable () -> Unit ): Composition { ... val original = Composition(UiApplier(owner.root), parent) // 構建Composition 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) } // 包裝成WrappedComposition wrapped.setContent(content) return wrapped } ```

content 函數 (例如 Text | Button ) 最終調用了 Layout 函數,核心邏輯就是通過 ReusableComposeNode 創建 Node 節點。

kotlin @Composable inline fun Layout( content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current ReusableComposeNode<ComposeUiNode, Applier<Any>>( factory = ComposeUiNode.Constructor, // factory創建Node節點 update = { // update更新Node節點內容 set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) }, skippableUpdate = materializerOf(modifier), content = content ) }

從上面我們可以看出來,Compose UI 如何基於 Compose Runtime 構建的具有樹管理的 View 系統(內部 LayoutNode 測量和繪製邏輯先忽略掉),下面我們來基於 Compose Runtime 構建一個簡單的樹管理系統,比如實現下面這個簡單的 Content 函數

kotlin @Composable fun Content() { var state by remember { mutableStateOf(true) } LaunchedEffect(Unit) { delay(3000) state = false } if (state) { Node1() } Node2() }

  1. 我們先定義 Node 節點(其中 Node1 和 Node2 都繼承於 Node,Node 內部通過 children 保存子節點信息)

```kotlin sealed class Node { val children = mutableListOf()

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

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

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

} ```

  1. 其次我們需要自定義 NodeApplier 用來操作 Node 節點

```kotlin class NodeApplier(node: Node) : AbstractApplier(node) { ... override fun insertTopDown(index: Int, instance: Node) { current.children.add(index, instance) // 插入節點 }

override fun move(from: Int, to: Int, count: Int) {
    current.children.move(from, to, count) // 更新節點
}

override fun remove(index: Int, count: Int) {
    current.children.remove(index, count) // 移除節點
}

} ```

  1. 然後我們需要定義 Compose 函數,(內部邏輯是通過 ReusableComposeNode 創建 Node 節點)

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

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

  1. 最後我們來運行 Content 函數,這樣我們就利用 Compose Runtime 構建了一個簡單的樹管理系統

```kotlin fun main() { val composer = Recomposer(Dispatchers.Main)

GlobalSnapshotManager.ensureStarted() // 監聽
val mainScope = MainScope()
mainScope.launch(DefaultChoreographerFrameClock) {
    composer.runRecomposeAndApplyChanges() // Choreographer Frame回調時開始重組
}

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

} ```

看到這裏我們大概明白了 Compose 構建流程,但是我們心中可能還有一些疑問:

  • Compose 函數內部調用流程是什麼樣的
  • Compose 怎麼構建生成 NodeTree,Node 節點信息怎麼儲存的
  • Compose 什麼時候發生重組,重組過程中做了什麼事情
  • Compose 如何監聽 State 變化並實現高效 diff 更新的
  • Snapshot 的作用是什麼

下面讓我們帶着上面這些疑問,看看 Kotlin Compiler Plugin 編譯後生成的代碼

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

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

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

}

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

第一次看到上面的代碼可能會有點懵,生成的 compose 函數內部插入了很多 $composer.startXXXGroup$composer.endXXXGroup 模板代碼,通過查看 Composer 實現類 ComposerImpl ,會發現所有 startXXXGroup 代碼最終調用下面這個 start 方法

kotlin /** * @param key: 編譯器生成Group唯一值 * @param objectKey: 輔助key,某些Group中會用到 * @param isNode: 是否有Node節點 * @param data: */ private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) { ... // slotTable操作邏輯 }

start 方法內部核心邏輯是通過 SlotReaderSlotWriter 操作 SlotTable,上述 Compose 函數內部生成的 $composer.startXXXGroup$composer.endXXXGroup 模板代碼就是構建 NodeTree,在 Composer 中針對不同的場景,可以生成不同類型的 Group。

| startXXXGroup | 説明 | | ----------------------------- | ------------------------------------------------------------ | | startNode /startResueableNode | 插入一個包含 Node 的 Group。例如文章開頭 ReusableComposeNode 的例子中,顯示調用了 startResueableNode ,而後調用 createNode 在 Slot 中插入 LayoutNode | | startRestartGroup | 插入一個可重複執行的 Group,它可能會隨着重組被再次執行,因此 RestartGroup 是重組的最小單元 | | startReplacableGroup | 插入一個可以被替換的 Group,例如一個 if/else 代碼塊就是一個 ReplaceableGroup,它可以在重組中被插入後者從 SlotTable 中移除 | | startMovableGroup | 插入一個可以移動的 Group,在重組中可能在兄弟 Group 之間發生位置移動 | | startReusableGroup | 插入一個可複用的 Group,其內部數據可在 LayoutNode 之間複用,例如 LazyList 中同類型的 Item |

接下來我們來看看 SlotTable 內部結構:

SlotTable

SlotTable 內部存儲結構核心的就是 groups ( group 分組信息,NodeTree 樹管理)和 slots ( group 所對應的數據),那 SlotTable 是怎麼實現樹結構和如何管理的呢?

```kotlin internal class SlotTable : CompositionData, Iterable { /* * An array to store group information that is stored as groups of [Group_Fields_Size] * elements of the array. The [groups] array can be thought of as an array of an inline * struct. / var groups = IntArray(0) private set

/**
 * An array that stores the slots for a group. The slot elements for a group start at the
 * offset returned by [dataAnchor] of [groups] and continue to the next group's slots or to
 * [slotsSize] for the last group. When in a writer the [dataAnchor] is an anchor instead of
 * an index as [slots] might contain a gap.
 */
var slots = Array<Any?>(0) { null }
    private set

} ```

groups 是一個 IntArray,每 5 個 Int 為一組構成一個 Group 的信息

  • key : Group 在 SlotTable 中的標識,在 Parent Group 範圍內唯一
  • Group info: Int 的 Bit 位中存儲着一些 Group 信息,例如是否是一個 Node,是否包含 Data 等,這些信息可以通過位掩碼來獲取。
  • Parent anchor: Parent 在 groups 中的位置,即相對於數組指針的偏移(樹結構
  • Size: Group: 包含的 Slot 的數量
  • Data anchor:關聯 Slot 在 slots 數組中的起始位置(位置信息

我們可以通過 SlotTable#asString() 方法打印對應的樹結構信息,通過前面分析,我們知道樹結構是在 Kotlin Compiler Plugin 編譯器生成的,通過 $composer#startXXXGroup$composer#endXXXGroup 配對生成 Group 樹結構。

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

GapBuffer

GapBuffer(間隙緩衝區)這個概念一般在很多地方有用到,比如文本編輯器,它在內存中使用扁平數組(flat array)實現,這個數組比真正存儲數據的集合要大,而且在插入數據的會判斷數據大小進行 gap 擴容,通過移動 gap index 可以將 insert(增)、delete(刪)、update(改)、get(查)操作的時間複雜度降到 O(n)常數量級。

SlotTable 中移動 gap 的方法詳見 moveGroupGapTo 和 moveSlotGapTo

下面我們來對比下沒有 GapBuffer 和 GapBuffer 兩種場景下刪除一個節點和多個節點的效率,可以看到刪除多個節點情況下 GapBuffer的效率要遠高於沒有 GapBuffer;在沒有 GapBuffer 的情況下,在 Array 中只能每次移動一個 Node,insert 和 delete 節點時間效率是 O(nLogN),但是有 GapBuffer 情況下,可以通過移動 gap 的位置,將時間效率優化到 O(n)。

| | 沒有GapBuffer | 有GapBuffer | | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 刪除一個節點 | | | | 刪除多個節點 | | |

Snapshot

Snapshot 是一個 MVCC(Multiversion Concurrency Control,多版本併發控制)的實現,一般 MVCC 用於數據庫中實現事務併發,還有分佈式版本控制系統(常見的 Git 和 SVN),下面簡單看下 Snapshot 使用。

```kotlin fun test() { // 創建狀態(主線開發) val state = mutableStateOf(1)

// 創建快照(開分支) val snapshot = Snapshot.takeSnapshot()

// 修改狀態(主線修改狀態) state.value = 2

println(state.value) // 打印1

snapshot.enter {//進入快照(切換分支) // 讀取快照狀態(分支狀態) println(state.value) // 打印1 } // snapshot.apply() 保存快照(下面print statr打印1)

// 讀取狀態(主線狀態) println(state.value) // 打印2

// 廢棄快照(刪除分支) snapshot.dispose() } ```

另外Snapshot提供了 registerGlobalWriteObserverregisterApplyObserver 用來監聽全局 Snapshot 寫入和 apply 回調,實際同時在 MutableSnapshot 構造函數傳入的。

kotlin open class MutableSnapshot internal constructor( id: Int, invalid: SnapshotIdSet, override val readObserver: ((Any) -> Unit)?, // 讀取監聽 override val writeObserver: ((Any) -> Unit)? // 寫入監聽 ) : Snapshot(id, invalid)

如果不直接複用系統封裝好的,我們也可以自己創建 Snapshot,並註冊通知。

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

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

回到我們之前提到的 GlobalSnapshotManager.ensureStarted(),實際上就是通過 Snapshot 狀態改變通知 Composition 重組。

```kotlin 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() // 發送通知applyChanges
            }
        }
        Snapshot.registerGlobalWriteObserver {
            channel.trySend(Unit) // 監聽全局Snapshot寫入
        }
    }
}

} ```

上面大概瞭解了 SlotTable 結構和 NodeTree 構建流程,下面看看這段代碼:

kotlin @Composable fun Content() { var state by remember { mutableStateOf(true) } LaunchedEffect(Unit) { delay(3000) state = false } ... }

估計大家應該能看懂這段代碼邏輯是創建一個 state,然後在3秒後更新 state 的值,但是大家一定存在幾個疑惑

  • remember 函數的作用是什麼
  • LaunchedEffect 函數作用是啥,裏面可以調用 delay 函數,是不是與協程有關係
  • 通過 mutableStateOf 創建的 State,為啥可以通知 Compose 進行重組

上面涉及到的 remember | LaunchedEffect | State 與 Compose 重組存在緊密聯繫,下面讓我們一起來看看 Compose 重組是如何實現的

Compose重組

@Composable 函數是純函數,純函數是冪等的,唯一輸入對應唯一輸出,且不應該包含任何副作用(比如修改全局變量或反註冊監聽等),為了維護 @Composable 純函數語義,Compose提供了 state、remember、SideEffect、CompositionLocal 這些實現,類似於 React 提供的各種 Hook。

在這裏插入圖片描述

Remember

直接來看下 remember 函數定義,主要參數是 key 和 calculation,Composer 根據 key 變化判斷是否重新調用 calculation 計算值

kotlin inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T inline fun <T> remember(key1: Any?, calculation: @DisallowComposableCalls () -> T): T inline fun <T> remember(key1: Any?, key2: Any?, calculation: @DisallowComposableCalls () -> T): T inline fun <T> remember(key1: Any?, key2: Any?, key3: Any?, calculation: @DisallowComposableCalls () -> T): T inline fun <T> remember(vararg keys: Any?, calculation: @DisallowComposableCalls () -> T): T

remember 內部調用的 composer#cache 方法,key 是否變化調用的 composer#changed 方法。

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

@ComposeCompilerApi override fun changed(value: Any?): Boolean { return if (nextSlot() != value) { updateValue(value) true } else { false } } ```

rememberedValue 直接調用 nextSlot 方法,updateRememberedValue 直接調用 updateValue 方法,核心邏輯就是通過SlotReaderSlotWriter 操作 SlotTable 存儲數據,而且這些數據是可以跨 Group 的,具體細節可以自己查看源碼。

State

State 接口定義很簡單,實際開發過程中都是調用 mutableStateOf 創建 MutableState

```kotlin fun mutableStateOf( value: T, policy: SnapshotMutationPolicy = structuralEqualityPolicy() // snapshot比較策略 ): MutableState = createSnapshotMutableState(value, policy)

internal actual fun createSnapshotMutableState( value: T, // SnapshotMutationPolicy有三個實現StructuralEqualityPolicy(值相等)|ReferentialEqualityPolicy(同一個對象)|NeverEqualPolicy(永不相同) policy: SnapshotMutationPolicy ): SnapshotMutableState = ParcelableSnapshotMutableState(value, policy) ```

ParcelableSnapshotMutableState 繼承自 SnapshotMutableStateImpl,自身實現 Parcelable 內存序列化,所以我們直接分析 SnapshotMutableStateImpl

```kotlin internal open class SnapshotMutableStateImpl( value: T, override val policy: SnapshotMutationPolicy ) : StateObject, SnapshotMutableState { @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 } } }

private var next: StateStateRecord<T> = StateStateRecord(value) // 繼承StateRecord

override val firstStateRecord: StateRecord
    get() = next

override fun prependStateRecord(value: StateRecord) {
    @Suppress("UNCHECKED_CAST")
    next = value as StateStateRecord<T>
}

@Suppress("UNCHECKED_CAST")
override fun mergeRecords(
    previous: StateRecord,
    current: StateRecord,
    applied: StateRecord
): StateRecord? {
        ... 
    // snapshot分支衝突解決合併邏輯,最終結果與policy相關
}

} ```

可以看到真正的核心類是 StateObject,StateObject 內部存儲結構是 StateRecord,內部使用鏈表存儲,通過 Snapshot 管理 State 值,最終調用 mergeRecords 處理衝突邏輯(與 SnapshotMutationPolicy 值相關)。

```kotlin abstract class StateRecord {

internal var snapshotId: Int = currentSnapshot().id  // snapshotId,版本管理

internal var next: StateRecord? = null // 內部存儲結構是鏈表

abstract fun assign(value: StateRecord)  // 將value賦值給當前StateRecord

abstract fun create(): StateRecord  // 創建新的StateRecord

} ```

SideEffect

副作用是指 Compose 內部除了狀態變化之外的應用狀態的變化,比如頁面聲明週期 Lifecycle 或廣播等場景,需要在頁面不可見或廣播註銷時改變一些應用狀態避免內存泄漏等,類似於 Coroutine 協程中提供的 suspendCancellableCoroutineinvokeOnCancel 中做一些狀態修改的工作,Effect 分為以下三類:

第一類是 SideEffect,實現方式比較簡單,調用流程是 composer#recordSideEffect -> composer#record, 直接往 Composer 中 changes 插入 change,最終會在 Composition#applychanges 回調 effect 函數。

kotlin @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) fun SideEffect( effect: () -> Unit ) { currentComposer.recordSideEffect(effect) }

```kotlin internal class CompositionImpl( ... ) : ControlledComposition {
... override fun applyChanges() { synchronized(lock) { val manager = RememberEventDispatcher(abandonSet) // RememberManager實現類 try { applier.onBeginChanges()

            // Apply all changes
            slotTable.write { slots ->
                val applier = applier
                // 遍歷changes然後invoke注入,可以查看ComposerImpl#recordSideEffect方法
                changes.fastForEach { change -> 
                    change(applier, slots, manager)
                }
                changes.clear()
            }

            applier.onEndChanges()

            // Side effects run after lifecycle observers so that any remembered objects
            // that implement RememberObserver receive onRemembered before a side effect
            // that captured it and operates on it can run.
            manager.dispatchRememberObservers() // RememberObserver的onForgotten或onRemembered被調用
            manager.dispatchSideEffects() // SideEffect調用

            if (pendingInvalidScopes) {
                pendingInvalidScopes = false
                observations.removeValueIf { scope -> !scope.valid }
                derivedStates.removeValueIf { derivedValue -> derivedValue !in observations }
            }
        } finally {
            manager.dispatchAbandons() // RememberObserver的onAbandoned被調用
        }
        drainPendingModificationsLocked()
    }
}
...

} ```

第二類是 DisposableEffect,DisposableEffectImpl 實現了 RememberObserver 接口,藉助於 remember 存儲在 SlotTable 中,並且 Composition 發生重組時會通過 RememberObserver#onForgotten 回調到 effectonDispose 函數。

kotlin @Composable @NonRestartableComposable fun DisposableEffect( key1: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult ) { remember(key1) { DisposableEffectImpl(effect) } }

第三類是 LaunchedEffect,與 DisposableEffect 的主要區別是內部開啟了協程,用來異步計算的。

kotlin @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.() -> Unit ) { val applyContext = currentComposer.applyCoroutineContext remember(key1) { LaunchedEffectImpl(applyContext, block) } }

CompositionLocal

WrappedComposition#setContent 我們看到有調用 CompositionLocalProvider,在 ProvideCommonCompositionLocals 內部中定義了很多 CompositionLocal,主要功能是在 content 函數內部調用其他 Compose 函數時,可以快捷獲取一些全局服務。

```kotlin private class WrappedComposition( val owner: AndroidComposeView, val original: Composition ) : Composition, LifecycleEventObserver {

private var disposed = false
private var addedToLifecycle: Lifecycle? = null

@OptIn(InternalComposeApi::class)
override fun setContent(content: @Composable () -> Unit) {
    owner.setOnViewTreeOwnersAvailable {
        if (!disposed) {
            val lifecycle = it.lifecycleOwner.lifecycle
            lastContent = content
            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.setContent {
                                            ... 
                    CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
                        ProvideAndroidCompositionLocals(owner, content) // CompositionLocal注入
                    }
                }
            }
        }
    }
}

} ```

kotlin @Composable internal fun ProvideCommonCompositionLocals( owner: Owner, uriHandler: UriHandler, content: @Composable () -> Unit ) { CompositionLocalProvider( LocalAccessibilityManager provides owner.accessibilityManager, LocalAutofill provides owner.autofill, LocalAutofillTree provides owner.autofillTree, LocalClipboardManager provides owner.clipboardManager, LocalDensity provides owner.density, LocalFocusManager provides owner.focusManager, LocalFontLoader provides owner.fontLoader, LocalHapticFeedback provides owner.hapticFeedBack, LocalLayoutDirection provides owner.layoutDirection, LocalTextInputService provides owner.textInputService, LocalTextToolbar provides owner.textToolbar, LocalUriHandler provides uriHandler, LocalViewConfiguration provides owner.viewConfiguration, LocalWindowInfo provides owner.windowInfo, content = content ) }

CompositionLocal 作用是為了避免組合函數間傳遞顯式參數,這樣可以通過隱式參數傳遞給被調用的組合函數,其內部實現也是利用了 SlotTable 存儲數據。

```kotlin @Stable sealed class CompositionLocal constructor(defaultFactory: () -> T) { @Suppress("UNCHECKED_CAST") internal val defaultValueHolder = LazyValueHolder(defaultFactory)

@Composable
internal abstract fun provided(value: T): State<T> //

@OptIn(InternalComposeApi::class)
inline val current: T
    @ReadOnlyComposable
    @Composable
    get() = currentComposer.consume(this)  // 獲取當前CompositionLocalScope對應的值

} ```

定義好 CompositionLocal 之後,需要通過 CompositionLocalProvider 方法綁定數據,ProvidedValue 可以通過 ProvidableCompositionLocal 提供的中綴方法 provides 返回。

kotlin @Composable @OptIn(InternalComposeApi::class) fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) { currentComposer.startProviders(values) // 在SlotTable的groups插入key為providerKey和providerValuesKey的group數據 content() currentComposer.endProviders() }

接着來看下 CompositionLocal 如何獲取數據,通過代碼看到直接通過 composer#consume 返回,而 consume 方法內部最終還是通過 CompositionLocalMap (實際是一個 PersistentMap, State\> 結構)獲取數據,其在 SlotTable 中對應的 groupKey 是 compositionLocalMapKey

```kotlin @Stable sealed class CompositionLocal constructor(defaultFactory: () -> T) { ... inline val current: T @ReadOnlyComposable @Composable get() = currentComposer.consume(this) }

internal class ComposerImpl(...) { ... override fun consume(key: CompositionLocal): T = resolveCompositionLocal(key, currentCompositionLocalScope())

private fun <T> resolveCompositionLocal(
    key: CompositionLocal<T>,
    scope: CompositionLocalMap
): T = if (scope.contains(key)) {
    scope.getValueOf(key)
} else {
    key.defaultValueHolder.value
}
...

} ```

看到這裏我們大概明白了 CompositionLocal 實現邏輯:

  • 首先定義 CompositionLocal
  • 通過 CompositionLocalProvoder 方法在 compose 函數嵌入插入 composer#startProviderscomposer#endProviders ,最終在 SlotTable 存入數據
  • 通過 composer#consume 獲取之前在 SlotTable 中插入的數據
  • 在 Compose 函數內部可以重新賦值,不過只在自身和子 Compose 函數內部生效

CompositionLocal 有兩種實現,第一種是 StaticProvidableCompositionLocal,全局保持不變(比如 LocalDensity 屏幕像素密度不隨 Compose 函數層級而改變)。

```kotlin internal class StaticProvidableCompositionLocal(defaultFactory: () -> T) : ProvidableCompositionLocal(defaultFactory) {

@Composable
override fun provided(value: T): State<T> = StaticValueHolder(value) // 返回一個常量

} ```

第二種是 DynamicProvidableCompositionLocal,可以在 Compose 函數內部改變其值,然後通知 Compose 重組並獲取到最新的值。

```kotlin internal class DynamicProvidableCompositionLocal constructor( private val policy: SnapshotMutationPolicy, defaultFactory: () -> T ) : ProvidableCompositionLocal(defaultFactory) {

@Composable
override fun provided(value: T): State<T> = remember { mutableStateOf(value, policy) }.apply {
    this.value = value
} /// 通過remember返回 MutableState

} ```

總結

到這裏我們就基本明白了 Compose 是怎麼實現的,最後回到我們之前的問題:

  • Compose 函數內部調用流程是什麼樣的
  • Kotlin Compiler Plugin 在編譯階段幫助生成 $composer 參數的普通函數(有些場景還有帶有 $change 等輔助參數),內部調用的 Compose 函數傳遞 $composer 參數
  • Compose 怎麼構建生成 NodeTree,Node 節點信息怎麼儲存的
  • Kotlin Compiler Plugin 在 Compose 函數前後插入 startXXXGroupendXXXGroup 構建樹結構,內部通過 SlotTable 實現 Node 節點數據存儲和 diff 更新,SlotTable 通過 groups 存儲分組信息 和 slots 存儲數據
  • Compose 如何監聽 State 變化並實現高效 diff 更新的
  • MutableState 實現了 StateObject,內部藉助 Snapshot 實現內部值更新邏輯,然後通過 remember 函數存儲到 SlotTable 中,當 State 的值發生改變時,Snapshot 會通知到 Composition 進行重組
  • Compose 什麼時候發生重組,重組過程中做了什麼事情
  • 當 State 狀態值發生改變時,會藉助 Snapshot 通知到 Composition 進行重組,而重組的最小單位是 RestartGroup(Compose 函數編譯期插入的 $composer.startRestartGroup ),通過 Kotlin Compiler Plugin 編譯後的代碼我們發現,重組其實就是重新執行對應的 Compose 函數,通過 Group key 改變 SlotTable 內部結構,最終反映到 LayoutNode 重新展示到 UI 上
  • Snapshot 的作用是什麼
  • Compose 重組藉助了 Snapshot 實現併發執行,並且通過 Snapshot 讀寫確定下次重組範圍
參考資料:

本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!