Jetpack Compose 的原理简单解析

904 阅读13分钟

📌 一、Jetpack Compose 是什么?

Jetpack Compose 是 Google 为 Android 开发的一套 声明式 UI 框架,主要用 Kotlin 编写,具备响应式、组合式的特性。

声明式 (Declarative) 指的是:

  • 只需要描述界面应该是什么样子,而无需逐步告诉界面怎么变成这样。

  • 传统的 View 系统是命令式 (Imperative) :需要一步步告诉界面该怎么改变(如调用 setText()setVisibility())。


AndroidComposeView 显示,触摸反馈的实际处理者。

📌 二、Compose 核心思想与原理

Compose 的核心思想:数据驱动 UI

即 UI 完全依靠数据来驱动:

  • 状态(State)变化 → UI 重组(Recompose)→ 界面更新。

核心流程:

数据(State) → Compose 函数重组 → UI 树更新 → 屏幕刷新

Compose 中的 UI 函数称为 @Composable,它们可直接响应数据变化。

四个关键流程

ComponentActivity.setContent()

  • 创建出基本的上下文环境;
  • 创建出后台循环等待的协程,并订阅变量的修改事件,在变量发生修改之后触发局部的重组,进而触发刷新。

源码流程跟踪:

  • ComponentActivity.setContent:入口,负责视图层级的增删改查
  • ComposeView.setContent:保存 Composable,并在 attach 时创建 Composition
  • AbstractComposeView.setContent:真正实例化 AndroidComposeView + 建立 Composition
  • GlobalSnapshotManager:桥接 Snapshot 写入 → 帧末重组 的调度器
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  // 主要工作是初始化工作,提供各种上下文信息,给变量的设置做好了监听能让变量在变化的时候及时的正确的触发重组,
  // 以及重组过程中可以得到正确的,变量最新的值.
  // 布置好各种监听器,当变量改变的时候刷新界面,真正看变量如何显示的,是看大括号里边的内容。
  setContent {
    Text("Hello, Compose!")
  }
}

/**
 * 将 Compose 内容挂载到 ComponentActivity。
 *
 * [parent]:可选的父级 CompositionContext(如 Navigation、Dialog 等环境会传入)
 * [content]:@Composable 函数,真正要渲染的 UI
 */
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    // Android 系统会把 Activity 的根视图包在一个 id 为 android.R.id.content 的 FrameLayout 里
    // 这里尝试复用之前的 ComposeView,避免重复创建 Composition。
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        // 走“复用”分支:更新父 Composition 环境 + 新的 content lambda
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // 👉 走“新建”分支:先配置,再挂进视图树

        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(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
        setOwners() // 把 LifecycleOwner/ViewModelStoreOwner 等注入到 ViewTree 中

        // 真正把 ComposeView 加到 Activity 的 DecorView 里
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

/**
 * ComposeView 内部暴露的简易 API。
 * 直接把 @Composable lambda 存到内部的 'content' State,
 * 等视图 attach 到 Window 时才创建 Composition。
 */
fun setContent(content: @Composable () -> Unit) {
    shouldCreateCompositionOnAttachedToWindow = true    // 标记:attach 时必须创建 Composition
    this.content.value = content                        // 保存 Composable 函数
    if (isAttachedToWindow) {                           // 如果已经 attach,则立刻创建
        createComposition()
    }
}

/**
 * AbstractComposeView 的核心设置入口:
 * 负责真正实例化 AndroidComposeView,并与父 CompositionContext 建立联系。
 */
internal fun AbstractComposeView.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    // ⚡️ 确保 Snapshot 全局监听器已启动(只会启动一次)
    GlobalSnapshotManager.ensureStarted()

    // 如果子 View 已经存在并且是 AndroidComposeView,复用;否则清空并新建
    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also {
            // 这里 addView 的其实是 AndroidComposeView.view —— 一个真实的 Android View
            addView(it.view, DefaultLayoutParams)
        }

    // 创建 Composition,返回给调用者
    return doSetContent(composeView, parent, content)
}

/**
 * 管理全局 Snapshot → Frame 重组通知的单例。
 *
 * Snapshot:Compose 用来跟踪可变状态(MutableState<T> 等)的机制;
 *          每次写操作都会产生“快照”,然后在帧末批量发布变更。
 */
internal object GlobalSnapshotManager {
    private val started = AtomicBoolean(false)

    // 下边的逻辑决定了变量更新的时候对特定的位置进行重组。
    fun ensureStarted() {
        if (started.compareAndSet(false, true)) {      // 保证只启动一次
            val channel = Channel<Unit>(Channel.CONFLATED)

            // 主线程协程,负责在每一帧把累积的 Snapshot 通知派发出去
            CoroutineScope(AndroidUiDispatcher.Main).launch {
                // 触发重组的逻辑
                channel.consumeEach {
                    Snapshot.sendApplyNotifications() // 通知 Compose “状态已变,请重组”
                }
            }

            // 对于变量值的写事件的监听。一帧里边只会触发一次或者两次。
            Snapshot.registerGlobalWriteObserver {
                // 任意线程写状态时,只是向 channel 发送信号;真正派发在主线程协程里
                channel.trySend(Unit)
            }
        }
    }
}

Layout()

  • 创建出 LayoutNode 对象
  • LayoutNode 对象插入到 SlotTable
  • 将排列好的 LayoutNode 插入到 LayoutNode 树,完成组合过程
  • 并非每个 @Composable 都会“最终通过 Layout() 创造 LayoutNode”。只有输出实际 UI 节点的 Composable(Text、Box、自定义绘制等)才会生成 LayoutNode 并挂到 LayoutNode 树。其它 Composable 要么只是组装/调用子 Composable,要么完全用于状态或副作用管理,对 UI 树没有直接影响。

只有 “真正往屏幕上画东西”的 Composable 才会 通过 Layout()(或等效 API)产生 LayoutNode;负责逻辑封装、状态控制或纯转发的 Composable 并不会额外创建节点。

类型举例是否一定创建 LayoutNode说明
UI-产生型 (emitter)Box { … }Text("Hi")Spacer()、自定义 Layout { … }这些函数内部最终会调用 Layout(...) / ComposeNode(...) / AndroidView(...) 等 API,向 SlotTable 写入一个 LayoutNode,并通过 Applier 把它挂到 LayoutNode 树中。
结构/组合型 (builder)Column { … }MyCard { … }(只是调用子 Composable)不一定如果函数自身不再调用 Layout(...),它只是在运行时展开并“转发”到真正产生 UI 的子 Composable;它本身不会生成新的 LayoutNode。
非 UI 型remember { … }LaunchedEffect(key) { … }SideEffect { … }不会这些函数只与状态、协程、副作用相关,对 UI 树没有任何节点输出。

LayoutNode 内部结构速览

LayoutNode
├─ parent: LayoutNode?                // 父节点
├─ children: SnapshotStateList<LayoutNode>
├─ measurePolicy: MeasurePolicy       // Text、Box、Row 等各自的测量算法
├─ modifier: Modifier                 // 用户传入的 Modifier 链
├─ layoutDelegate                     // 负责 diff + 包装链构建 + 测量/布局调度
│   ├─ innerLayoutNodeWrapper         // 最里层 Wrapper(InnerPlaceable)
│   └─ outerWrapper = …               // 包含所有 LayoutModifier 的链首
├─ placeOrder, zIndex                 // 层级顺序信息
├─ layer: OwnedLayer?                 // RenderNode / Canvas layer(可为空)
├─ pointerHandlers, semanticsEntities // 输入与辅助功能
└─ snapshotObserver                   // 跟踪状态读取,驱动自动重组/重测量

几个常见概念:

  1. LayoutNode 树

    • 每个 LayoutNode 可以有 0 ~ N 个子节点,整体结构是一棵树(与 ViewTree/DOM 树类似)。
    • Compose Runtime 通过 Applier 在 “top-down / bottom-up” 两个阶段把节点插入或移动到这棵树里。
  2. SlotTable 与 LayoutNode 解耦

    • SlotTable 保存的是“Composable 调用栈”及其 参数快照,用于重组 (recomposition) 时的 diff。
    • LayoutNode 存在于 UI 层,负责测量、布局、绘制。
    • 两者通过 Anchor 建立映射,但生命周期独立。
  3. ReusableComposeNode / ComposeNode / Layout

    • Layout() 只是 ComposeNode 的一个语法糖,用于创建具备 MeasurePolicyLayoutNode
    • 同时也可直接调用 ComposeNode { … },自定义更多属性写入逻辑。

LayoutNode 树 ≈ “Compose 世界里的 View Tree” ,但更轻量、层级更灵活,也完全服务于 Compose Runtime 的声明式重组模型。最终依旧只有一个根 AndroidComposeView 嵌入旧有 View 体系,保证与现有 Activity/Fragment 协同工作。

源码流程跟踪:

@Suppress("NOTHING_TO_INLINE")
@Composable
@UiComposable
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    // 👉 每帧都会把这些 CompositionLocal 值写进 LayoutNode,供测量/绘制使用
    val density = LocalDensity.current               // dp ⇄ px 转换 & fontScale
    val layoutDirection = LocalLayoutDirection.current // LTR / RTL
    val viewConfiguration = LocalViewConfiguration.current // 触摸 slop、长按时长等

    // 👉 将 Modifier.chain() 等 Runtime 修饰符序列化成稳定对象,避免重复工厂调用
    val materialized = currentComposer.materialize(modifier)

    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        // 将一个 Composable 对象包装成 一个LayoutNode。
        factory = ComposeUiNode.Constructor,
        update = {
            // 👉 set(值, “回调接口”):只在值变化时写入,节省 Snapshot 开销
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
            set(materialized, ComposeUiNode.SetModifier)
        },
    )
}

/* ────────────────────────────────────────────────────────────── */
/*  ↓↓↓ 下面是 LayoutNode 的默认构造器 ↓↓↓                         */
/* ────────────────────────────────────────────────────────────── */

val Constructor: () -> ComposeUiNode = LayoutNode.Constructor

/**
 * Pre-allocated constructor to be used with ComposeNode
 */
internal val Constructor: () -> LayoutNode = { LayoutNode() }

createNode():Composer 插入阶段的三步曲

/**
 * Schedule a node to be created and inserted at the current location. This is only valid to
 * call when the composer is inserting.
 */
@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) // 👉 当前 SlotTable group 的锚点
    groupNodeCount++

    /* ---------- Top-Down 插入阶段:创建对象 + 写 SlotTable ---------- */
    recordFixup { applier, slots, _ ->
        @Suppress("UNCHECKED_CAST")
        // 1. 创建 LayoutNode(或其他 UI 节点),工厂函数来自 ReusableComposeNode.factory
        val node = factory()

        // 2. 把刚刚创建的 LayoutNode 放进 SlotTable(树形 diff 用的持久化结构)
        slots.updateNode(groupAnchor, node)

        // 3. 通知 Applier 在“挂载阶段”把节点插到 View/RenderTree 中
        @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
        nodeApplier.insertTopDown(insertIndex, node)

        applier.down(node) // 👉 进入该节点,准备递归处理子节点
    }

    /* ---------- Bottom-Up 插入阶段:收尾,把节点真正挂到父节点 ---------- */
    recordInsertUpFixup { applier, slots, _ ->
        @Suppress("UNCHECKED_CAST")
        val nodeToInsert = slots.node(groupAnchor) // SlotTable 中刚写入的那个节点
        applier.up()                               // 结束对子节点的递归处理

        @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<Any?>
        // 3. 把 LayoutNode 插进 LayoutNode 树里边(最终挂载到 ViewHierarchy/RenderTree)
        nodeApplier.insertBottomUp(insertIndex, nodeToInsert)
    }
}

流程回顾

  1. Top-Down

    • 创建 LayoutNode(或其它 UI 节点)。
    • 写入 SlotTable,为下一轮重组提供 diff 依据。
    • insertTopDown() 通知 Applier:父节点即将拥有一个新孩子。
  2. 递归处理子节点 (applier.downapplier.up)

  3. Bottom-Up

    • insertBottomUp() 真正把子节点 attach 到父节点的子列表,视图树就此成型。

Snapshot

  • Compose 里对于变量变化的管理机制,可以让变量改变的时候发出通知、触发重组,并且在重组时应用到正确的变量值
  • 支持多线程并发写入新值

SlotTable

  • Compose 内部对 LayoutNode 树、Modifier 等属性、用到的变量进行存储的数据结构;由于是一维数组管理,性能非常强
  • 最终的 LayoutNode 树不是由 SlotTable 承载,SlotTable 只用于中间计算过程

SlotTable ↔️ Applier ↔️ LayoutNode 的协作关系是怎么样的?

我们把 SlotTable → Applier → LayoutNode 的合作流程拆成 ❶「谁负责什么」、❷「第一次组合」、❸「重组 diff」、❹「为什么要分两段调用」、❺「一张心智图」五部分,更加容易理解。


❶ 三个角色各管什么

角色关键词职责
SlotTable声明式快照持久化数组树 记录「某个 Composable 在哪、参数值是什么、它是否应该生成节点」。 它是 Composer 的账本:只描述 意图,不直接改 UI。
Applier命令式执行器把 Composer 的“增删移改”指令落实到目标树(ViewTree / LayoutNodeTree / DOM 等)。 Compose UI 默认实现是 LayoutNodeApplier
LayoutNode真正的 UI 节点承载测量/布局/绘制、事件分发,构成一棵 UI 逻辑树。 每个节点背后还挂着 ModifierMeasurePolicy 等运行时数据。

❷ 初次组合(Composition)── 树是怎么“长”出来的?

sequenceDiagram
    autonumber
    participant Cmp as Composable函数
    participant CP as Composer (写 SlotTable)
    participant AP as Applier (指挥)
    participant LN as LayoutNode树

    Cmp->>CP: Layout { ... }  // 遇到节点工厂
    CP-->>SlotTable: ① 记录 createNode 指令(写 anchor 等元数据)
    Note over CP: 在 writer 阶段先写 SlotTable,并把“改动”登记成 Fixup

    CP->>AP: Fixup Top-down, insertTopDown(idx, new LN)
    AP->>LN: ② new LayoutNode() <br>挂到父.children[idx](临时)
    AP->>AP: down() ——游标进入新节点

    loop 递归子树
        Cmp->>CP: ... (child composables)
    end

    AP->>AP: up() ——回到父游标
    CP->>AP: Fixup Bottom-up, insertBottomUp(idx)
    AP->>LN: ③ 将节点正式 attach
  • ① 写 SlotTable:保存调用栈 + 参数快照,生成 anchor
  • ② insertTopDown:实例化节点 & 预占坑。
  • ③ insertBottomUp:子树全部处理完,再真正 attach,保持树的一致性。

❸ 重组(Recomposition)── 什么在变?谁来动?

步骤发生在哪细节
1. 状态读写SnapshotMutableState 写入 → GlobalSnapshotManager 下一帧触发重组
2. 重新执行 ComposableComposer带着旧 SlotTable 重新跑;边跑边 diff: 比对 groupKey、参数标记、skip 信息
3. 产生 ChangeListComposer对每个 group 给出 insert / remove / move / update 四类指令
4. Applier 执行Applier- insertTopDown / insertBottomUp 新节点 - remove / removeUp 删除 - move / moveUp 调序 - update 只改属性(MeasurePolicy / Modifier 等)
5. Layout/Draw 阶段LayoutNode只要大小或位置相关属性变更,就走 measure/layout;否则只 draw 或甚至完全跳过

SlotTable 更新完成后即成为下一次 diff 的“旧表”;
LayoutNode 只在真正需要时才增删移,极大节省对象 churn。


❹ 为什么一定要 Top-Down + Bottom-Up 两段调用?

  1. 保证父先知子 — 渲染树逻辑上必须父存在后再插子,但在 diff 过程中创建时机难以保证;拆成两段即可先“标记”后“落地”。
  2. 方便移动节点move() 可以用一次 topDown 占坑、一次 bottomUp attach,而不用真正删除/新建。
  3. 统一多后端 — View、SVG、Canvas 等后端都可用同一 Applier 接口,只需实现局部插拔逻辑。

❺ 心智图:一次重组的最短路线

MutableState.write()
      │
 [Snapshot]         (收集变更)
      ▼
GlobalSnapshotManager.sendApplyNotifications()
      │
      ▼
┌─► Composer.recompose(rootAnchor)
│     │  (exec composable & diff SlotTable)
│     ▼
│  changeList:  [{insert},{update},…]
│     │
└──► Applier.applyChanges(changeList)
         │
         ▼
   LayoutNode tree updated

一句话总结:

SlotTable 记录“声明式想法”,Applier 把想法“命令式兑现”,最终落在轻量的 LayoutNode 树上;三者解耦,使得 Compose 能用最小成本响应状态变化,又方便切换不同渲染后端。


小结

  • 调试树结构LayoutInspectorModifier.layoutId() + print*() 系列。
  • 避免多余节点:用 Modifier 而非层层包裹的 Box {},否则 LayoutNode 数增多会加大 measure/layout 成本。
  • 自定义 Applier:想把 Compose 输出到 Canvas、GL 或 Web,也只需实现对应 Applier 和 UI 节点类。

📌 三、Compose 编译器做了什么?

Compose 的秘密武器之一,是它的 编译器插件 (Compose Compiler)

  • 原理:在编译期把声明式的 UI 代码转换成高效的增量式更新逻辑

例如,你写的 Compose 代码:

@Composable
fun MyText(name: String) {
    Text("Hello, $name!")
}

编译器会转换为(示意,非真实代码):

fun MyTextComposer(name: String, composer: Composer) {
    composer.startGroup()
    if (composer.changed(name)) {
        composer.emitText("Hello, $name!")
    }
    composer.endGroup()
}

通过这样的转换,实现高效的局部刷新(只更新变化的部分)。


📌 四、Compose 的三棵树(Compose Three Trees)

Compose 的底层原理通过三棵树来实现高效更新:

树名称描述作用
Composition树(组合树)@Composable函数调用形成的结构树记录UI结构和组合关系(决定重组)
Layout树(布局树)测量与布局阶段产生的树结构测量、定位组件
Drawing树(绘制树)绘制指令(Drawing commands)的树形结构真正进行绘制操作(渲染到屏幕)

三树流程图:

Composition树 → Layout树 → Drawing树 → 显示到屏幕

📌 五、Compose 更新的四个阶段

Compose 每次更新经历以下四个阶段:

(1) Composition(组合阶段)

  • 确定界面的结构,包括调用了哪些 Composable 函数。
  • 只会调用数据变化所影响的部分(增量重组)。
数据变化 → 触发重组(Recompose)

(2) Measure(测量阶段)

  • 确定每个节点的宽高尺寸。
确定节点尺寸

(3) Layout(布局阶段)

  • 确定每个节点在屏幕中的位置(坐标)。
确定节点位置

(4) Drawing(绘制阶段)

  • 根据布局信息执行绘制命令。
真正绘制到屏幕

📌 六、状态管理(State Management)

Compose 的状态是通过 State<T> 封装的:

val count by remember { mutableStateOf(0) }
  • 状态变化会自动触发重组(Recompose)。
  • Compose 会跟踪哪些 UI 依赖了特定的状态,状态变更时只触发这些 UI 重组。

示例:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
  • 每次点击按钮修改状态时,只有依赖了 count 的 Composable 部分 (Text) 重组,而不会整个界面全部重绘。

📌 七、副作用(Side Effects)处理机制

Compose 中副作用(如网络请求、启动协程、监听事件)通常使用:

  • LaunchedEffect:运行挂起函数(如网络请求)。
  • DisposableEffect:对副作用进行生命周期管理。

示例:

@Composable
fun Timer() {
    var seconds by remember { mutableStateOf(0) }

    LaunchedEffect(Unit) { // 副作用
        while (true) {
            delay(1000)
            seconds++
        }
    }

    Text("Seconds: $seconds")
}

📌 八、与 Android 原生 View 的集成原理

Compose 与原生 View 通过以下组件互相嵌套:

  • AndroidView:在 Compose 中引入传统View。
  • ComposeView:在传统 View 中引入 Compose UI。

架构:

Compose → AndroidView → 原生View
传统View → ComposeView → Compose
  • 事件传播会自动进行桥接和转换。

📌 九、Jetpack Compose 事件传递机制(补充说明)

Compose 中触摸事件分三个阶段传播:

  • Initial (父→子) :祖先先拦截事件。
  • Main (子→父) :子节点优先消费事件。
  • Final (父→子) :子节点明确知道父节点是否消费事件。

这样事件传播逻辑更清晰,更可控。


🚩 十、Compose 与传统 View 的核心差异总结

对比维度Android ViewJetpack Compose
编程模式命令式(手动更新)声明式(自动更新)
更新方式手动调用setXXX方法状态变化自动驱动
重绘粒度粗粒度(通常手动)细粒度(自动增量更新)
性能优化依赖开发者编译器自动优化
事件传播机制dispatch/onIntercept/onTouch三阶段显式事件机制
状态管理手动维护状态内置State机制

🎯 十一、小结(Compose核心原理一句话)

Jetpack Compose 是一个声明式、基于状态自动驱动、增量更新的UI框架,通过编译器插件和三树结构,自动高效地进行局部重组与刷新,帮助开发者以更直观、更简洁的方式开发UI。