上一篇聊了 Media3,这篇回到 UI 方向。Compose 的声明式 UI 看着简单,但做到复杂布局和自定义 Modifier 时,不理解底层原理就会卡住。这篇从 Layout 协议到 Modifier 链,拆解 Compose 布局的核心机制。
Compose 布局三阶段
Compose 的渲染流程分三个阶段:
组合(Composition):执行 @Composable 函数,生成 UI 树(节点树)
布局(Layout):测量每个节点的尺寸和位置
绘制(Draw):把节点画到屏幕上
每个阶段都有对应的性能优化手段。比如 remember 减少重组,deferRead 延迟状态读取减少布局,drawWithCache 减少绘制。
布局协议
Compose 的布局是一个"先测量子节点,再确定自身尺寸"的过程,类似 View 体系的 measure/layout,但约束传递方式不同。
核心接口是 Layout composable:
@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// measurables: 子节点列表
// constraints: 父节点传下来的约束(最小/最大宽高)
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算自己的尺寸
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
layout(width, height) {
var y = 0
placeables.forEach { placeable ->
placeable.place(0, y)
y += placeable.height
}
}
}
}
这是 Compose 自定义布局的核心模板。measure 返回 Placeable,place 设置子节点位置。
自定义纵向布局示例
写一个带间距的纵向布局(Column 默认不支持 item 间距):
@Composable
fun SpacedColumn(
spacing: Dp,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val spacingPx = spacing.roundToPx()
val placeables = measurables.map { it.measure(constraints) }
val height = placeables.sumOf { it.height } +
spacingPx * (placeables.size - 1).coerceAtLeast(0)
val width = placeables.maxOf { it.width }
layout(width, height) {
var y = 0
placeables.forEach { placeable ->
placeable.place(0, y)
y += placeable.height + spacingPx
}
}
}
}
自定义 Modifier
Modifier 是 Compose 的链式装饰器。每个 Modifier 节点负责一部分布局或绘制工作。
fun Modifier.customBackground(color: Color): Modifier = this.then(
Modifier.drawBehind {
drawRect(color)
}
)
drawBehind 是绘制型 Modifier。还有布局型 Modifier 用 layout:
fun Modifier.firstBaselineToTop(distance: Dp): Modifier = this.then(
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// 获取第一行文字的基线
val firstBaseline = placeable[FirstBaseline]
val distancePx = distance.roundToPx()
val y = distancePx - firstBaseline
layout(placeable.width, placeable.height + y) {
placeable.place(0, y)
}
}
)
Modifier 链的执行顺序
Modifier 链的执行顺序是从外到内:
Box(
modifier = Modifier
.padding(16.dp) // 1. 先执行 padding
.background(Color.Red) // 2. 然后画背景
.size(100.dp) // 3. 再设尺寸
.clip(CircleShape) // 4. 最后裁剪
)
顺序不同,效果完全不同。padding 在 background 前面意味着背景画在 padding 区域外,反之背景画到 padding 区域内。
性能优化:避免不必要的测量
Compose 布局有"不重复测量同深度子节点"的规则(Intrinsic Measurements 除外)。这意味着自定义布局时,measure 的调用次数是可控的。
但如果你写了这样的布局:
// 反面教材:每个子节点测量两次
Layout(content) { measurables, constraints ->
val firstPass = measurables.map { it.measure(constraints) }
val maxWidth = firstPass.maxOf { it.width }
val secondPass = measurables.map {
it.measure(constraints.copy(maxWidth = maxWidth))
}
// ...
}
两次测量会导致性能问题。解决方案是使用 Intrinsic Measurement:
Layout(content) { measurables, constraints ->
// 先用 minIntrinsicWidth 获取理想宽度
val idealWidth = measurables.maxOf {
it.minIntrinsicWidth(constraints.maxHeight)
}
val placeables = measurables.map {
it.measure(constraints.copy(maxWidth = idealWidth))
}
// 只测量一次
}
SubcomposeLayout
当布局需要根据子节点的尺寸决定显示哪些子节点时(比如 LazyColumn 只渲染可见项),用 SubcomposeLayout:
@Composable
fun SimpleLazyList(content: @Composable () -> Unit) {
SubcomposeLayout { constraints ->
// 可以在布局过程中动态创建子节点
val items = subcompose("items", content)
items.forEach { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
// 放置
}
}
}
LazyColumn、LazyRow 的底层就是 SubcomposeLayout。但它的开销比普通 Layout 大,只在需要动态子节点组合时用。
性能分析工具
Layout Inspector:Android Studio 内置,查看 Compose 树结构和重组次数
Compose Compiler Metrics:在
gradle.properties中开启:
composeCompilerReports=true
composeCompilerMetrics=true
生成的报告里可以看到每个 Composable 是否稳定(stable)、是否可跳过(skippable)。不稳定或不可跳过的函数会触发不必要的重组。
Macrobenchmark:量化布局性能变化
小结
Compose 自定义布局的核心是理解 Layout 协议(measure → layout → place)。Modifier 链的顺序决定视觉效果,从外到内执行。性能优化重点是减少不必要的测量和重组,用 Intrinsic Measurement 替代多次 measure 调用。复杂场景考虑 SubcomposeLayout。
下一篇聊 ViewBinding 与 DataBinding 的取舍。