扒一扒 Jetpack Compose 實現原理

雲音樂技術團隊發表於2022-11-23

圖片來自:https://developer.android.goo...

本文作者: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 引數。

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 文件的例子:

@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 方法。

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 節點。

@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 函式

@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 儲存子節點資訊)
sealed class Node {
    val children = mutableListOf<Node>()

    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 節點
class NodeApplier(node: Node) : AbstractApplier<Node>(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 節點)
@Composable
private fun Node1(name: String = "node1") {
    ReusableComposeNode<Node.Node1, NodeApplier>(
        factory = {
            Node.Node1()
        },
        update = {
            set(name) { this.name = it }
        }
    )
}

@Composable
private fun Node2(name: String = "node2") {
    ReusableComposeNode<Node.Node2, NodeApplier>(
        factory = {
            Node.Node2()
        },
        update = {
            set(name) { this.name = it }
        }
    )
}
  1. 最後我們來執行 Content 函式,這樣我們就利用 Compose Runtime 構建了一個簡單的樹管理系統
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 編譯後生成的程式碼

   @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 方法

/**
    * @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 是怎麼實現樹結構和如何管理的呢?

internal class SlotTable : CompositionData, Iterable<CompositionGroup> {
    /**
     * 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 樹結構。

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 使用。

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 建構函式傳入的。

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

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

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 重組。

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 構建流程,下面看看這段程式碼:

@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 計算值

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 方法。

inline fun <T> 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

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

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

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

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

    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 值相關)。

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 函式。

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
    effect: () -> Unit
) {
    currentComposer.recordSideEffect(effect)
}
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 函式。

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

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

@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 函式時,可以快捷獲取一些全域性服務。

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注入
                        }
                    }
                }
            }
        }
    }
}
@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 儲存資料。

@Stable
sealed class CompositionLocal<T> 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 返回。

@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<CompositionLocal<Any?>, State<Any?>> 結構)獲取資料,其在 SlotTable 中對應的 groupKey 是 compositionLocalMapKey

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

internal class ComposerImpl(...) {
      ...
        override fun <T> consume(key: CompositionLocal<T>): 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 函式層級而改變)。

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

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

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

internal class DynamicProvidableCompositionLocal<T> constructor(
    private val policy: SnapshotMutationPolicy<T>,
    defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(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!

相關文章