Composition:本质、在Compose中构建UI树

作者: IT Sectr 发布日期: 2026-06-27 阅读时间: 7 分钟

Composition — 是Jetpack Compose中的核心过程,通过这个过程的描述性Composable函数构建出显示在屏幕上的实时UI树。与Android的View系统不同(布局从XML加载并转换为不可变对象),Composition作为一个动态系统工作:函数被执行,在内存中创建插槽,形成节点层次结构,并将其与状态关联。根据Google Android Developers, 2026,理解Composition对于优化Compose应用程序的性能至关重要。

要点

  • Composition — 执行Composable函数以构建UI树
  • 插槽 — 存储每个函数参数和状态的内存单元
  • 位置在Compose中(Positional Memorization)将状态绑定到代码中的位置
  • 首次遍历 Composition在屏幕启动时创建初始UI树
  • CompositionLocal 通过树传递数据而无需显式参数

什么是Jetpack Compose中的Composition

Composition — 是执行Composable函数的过程,其结果是以节点树的形式形成用户界面的内部表示。该树的每个节点对应一个内置组件(Text、Button、Image)或者一个自定义Composable函数的调用。Composition不直接创建Android View对象——它构建一个抽象描述,然后由Layout和Drawing阶段处理。

Composition的关键特性是它的可重启性(restartability)。组合中的每个Composable函数可以在任何时候重新启动,如果其输入参数或它读取的状态对象发生更改。系统不会重新启动整个树——仅重启那些实际依赖于更改数据的函数。

从技术上讲,Composition通过Composer进行管理——Kotlin编译器嵌入到每个Composable函数中的内部引擎。Composer将关于哪些函数被调用、使用什么参数以及按什么顺序调用的信息写入插槽(位置组)。在后续调用中,Composer将新数据与保存的数据进行比较,并决定是否重新启动。

在Composition过程中UI树是如何构建的

构建UI树的过程从在Activity或Fragment中调用setContent方法开始。该方法创建初始Composition并启动根Composable函数的执行。然后每个嵌套的Composable函数将其节点添加到树中,形成层次结构:Row包含Text和Button,Column包含Image和Card,依此类推。

树的每个节点获得一个基于其在源代码中位置的唯一位置键。该键用于在重复执行时标识节点。位置键是Composable函数调用顺序不应依赖于条件的原因:如果一次执行中调用了A -> B,而下一次执行中调用了B -> A,Compose将无法匹配新旧节点。

kotlin
@Composable
fun AppScreen() {
    Column {                     // Column节点(位置1)
        HeaderSection()            // HeaderSection节点(位置2)
        ContentSection()           // ContentSection节点(位置3)
        FooterSection()            // FooterSection节点(位置4)
    }
}

@Composable
fun HeaderSection() {
    Row {                       // Row节点(位置2.1)
        Text("标题")         // Text节点(位置2.2)
        Icon(...)                // Icon节点(位置2.3)
    }
}

在这个例子中,每次调用都获得基于代码顺序的位置。Column(位置1)包含三个子节点(位置2、3、4)。HeaderSection又添加了两个子节点(2.1、2.2、2.3)。如果在接下来的重组合中ContentSection在HeaderSection之前被调用,Composer将无法正确匹配节点——因此规则是:Composable函数的调用顺序必须稳定。

Composition中的状态管理

Composition中的状态通过State<T>类型的对象进行管理。当Composable函数通过委托属性(by)从State读取值时,它会注册对该State的依赖。当值更改时,所有读取过该State的函数将被标记为在下一组合阶段重新启动。

依赖注册机制称为快照系统。每次State更改时,快照记录所有更改并通知Composer哪些函数依赖于该State。需要理解的是:在非Composable代码中(例如在onClick lambda中)读取State不会注册依赖——仅在Composable函数内部或组合上下文中执行的lambda中的读取才会注册。

快照系统以事务方式工作:一个事件中的多个State更改合并为一个事务,从而防止多次重组合。这在处理手势时尤其重要:在一次移动中多个State对象发生变化,但Compose只执行一次重组合。

kotlin
@Composable
fun StateExample() {
    var text by remember { mutableStateOf("Hello") }
    var isVisible by remember { mutableStateOf(true) }

    Column {
        Text(text)  // 注册对text的依赖

        if (isVisible) {  // 注册对isVisible的依赖
            TextField(value = text, onValueChange = { text = it })
        }

        Button(onClick = { isVisible = !isVisible }) {
            Text(if (isVisible) "隐藏" else "显示")
        }
    }
}

text的更改仅导致Column、Text和TextField的重组合。Button和isVisible条件保持不变。这种重组合的隔离性是Compose相较于重新绘制整个屏幕的系统的关键优势。每个Composable函数仅跟踪其直接读取的State对象。

Composition与Recomposition:主要区别

Composition(组合)和Recomposition(重组合)——是Composable函数执行的两种不同模式。Composition在创建屏幕时发生一次:系统使用初始值执行所有Composable函数并构建初始UI树。Recomposition在数据更改时多次发生:系统仅重启那些依赖于更改状态的函数。

Composition模式激活树的所有节点,为每个函数分配插槽,注册所有后代。Recomposition选择性工作:Compose比较每个函数参数的新旧值,如果没有变化——函数不执行(skipping)。

Composition和Recomposition的成本不同。首次Composition更昂贵,因为它需要完整的树构建和插槽分配。Recomposition更便宜,特别是如果大多数函数是稳定的——它们的参数通过equals进行比较,Compose跳过它们的调用。为了获得最大性能,应尽量减少受重组合影响的函数数量。

特性CompositionRecomposition
发生时间一次,首次显示时多次,数据更改时
范围整个树仅更改的函数
参数比较不执行为skipping执行
创建插槽是,所有插槽都被创建仅针对新节点

CompositionLocal:通过树传递数据

CompositionLocal — 通过组合树隐式传递数据的机制。它解决了一个参数需要通过数十个不直接使用它的嵌套Composable函数传递的问题。无需显式参数链,而是在上层设置数据,并通过CompositionLocal.current在任何嵌套函数中读取。

MaterialTheme主题——CompositionLocal最著名的例子。所有Compose组件通过MaterialTheme.colorScheme、MaterialTheme.typography、MaterialTheme.shapes读取颜色、排版和形状,而不通过参数接收它们。开发者可以为当前用户、本地化设置或屏幕配置等数据创建自己的CompositionLocal。

重要限制:CompositionLocal不应用于频繁变化的数据(滚动位置、输入字段中的文本)。读取CompositionLocal的组件在每次值更改时都会重新启动,因此对于动态数据,最好使用显式参数或State。CompositionLocal对于很少或从不更改的配置数据是最优的。

kotlin
val LocalUser = compositionLocalOf<User?> { null }

@Composable
fun AppRoot(user: User, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUser.provides(user)) {
        content()
    }
}

@Composable
fun UserAvatar() {
    val user = LocalUser.current  // 无需显式参数读取
    AsyncImage(model = user?.avatarUrl, contentDescription = "Avatar")
}

CompositionLocalProvider创建一个可见范围,在其中LocalUser.current返回设定的值。UserAvatar通过中间函数读取用户,无需显式传递参数。这在深层层次结构中尤其有价值,其中数据仅在几个叶节点中需要。

常见问题

如果在Composition期间更改State会发生什么?

在Composition期间更改State会安排一个新的重组合,将在当前重组合完成后执行。不会发生死循环:Compose保证每个重组合在快照系统的单独事务中执行。

复杂屏幕的Composition需要多长时间?

在现代设备上,具有50–100个Composable函数的屏幕的Composition需要1–5毫秒。Google建议在60fps时每帧保持在16毫秒内。如果Composition超过此限制,请使用LazyColumn或将屏幕拆分为更小的函数。

可以手动启动Composition吗?

无法直接手动启动Composition——它由Composer自动管理。但是,如果拥有对CompositionContext的访问权限,可以通过更改State或在根组合上调用invalidate()来强制安排重组合。

Composition与经典Android中的View层次结构有何不同?

View层次结构——是一个不可变的Java对象树,创建一次。Composition——是一个虚拟树,在每次数据更改时重建。View将其状态存储在实例变量中,Composition——存储在绑定到函数调用位置的插槽中。

Composition如何处理节点删除?

如果Composable函数不再被调用(例如if条件变为false),Composition删除其节点并调用DisposableEffect的清理。当再次出现时(if再次变为true),创建一个新节点——旧节点不会恢复。

总结

  • Composition — 执行Composable函数以构建与状态关联的UI树的过程
  • Composer 管理插槽,记录函数调用并在重组合时比较参数
  • 快照系统 注册函数对State的依赖并将更改合并到事务中
  • Composition 在启动时执行一次,Recomposition — 在数据更改时执行
  • CompositionLocal 通过树传递配置数据,无需显式参数链
  • 位置 函数调用位置作为其在组合树中的唯一标识符
  • 建议:创建具有不可变参数的小型Composable函数,以实现高效的skipping

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读