文章目录
- UI帧的3个阶段
- 状态读取
-
- 状态读取的两种访问方式
- 优化状态读取
- [重组循环 问题](#重组循环 问题)
UI帧的3个阶段
- 组合(Composition): 决定显示什么(执行 @Composable 函数,构建 UI 树)。
- 布局(Layout): 决定放在哪里(包含 测量-Measure和放置-Placement 两个子步骤)。
- 绘制(Drawing): 决定如何渲染绘制。
- 在组合阶段,Compose 运行时会执行可组合函数,并输出表示界面的树结构 。此界面树由包含后续阶段所需的所有信息的布局节点组成

- 在布局阶段,系统会使用以下三步算法遍历树:
- 测量子项:节点会测量其子项(如果有)。
- 确定自己的尺寸:节点根据这些测量结果确定自己的尺寸。
- 放置子节点:每个子节点都相对于节点自身的位置放置。
在此阶段结束时,每个布局节点都具有:
分配的宽度和高度;应绘制到的 x、y 坐标
- 绘制阶段,系统会再次从上到下遍历UI树,并且每个节点依次在屏幕上绘制自身
状态读取
在 Compose 中,状态(State)在哪里被读取,决定了当状态改变时,Compose 会从哪个阶段开始重新执行。
- 在组合阶段,读取到状态改变,即发生重组。完整重新执行 三个阶段:组合、布局、绘制。
简单理解组合阶段的状态:在 composable中,定义的 一些 remember 状态,它们一进入组合(或构建UI树时)就会执行,不在 布局和绘制阶段才执行
- 在布局阶段,读取到状态改变,跳过组合,重新执行 两个阶段:布局、绘制。
比如在 Modifier.offset {} 中读取了状态
- 在绘制阶段,读取到状态改变,跳过组合、布局,重新执行 绘制 阶段。
比如在 Canvas { drawRect(color = state.value) } 或者 Modifier.drawBehind { ... } 内部读取了颜色状态。
状态读取的两种访问方式
- 使用 = 进行状态赋值
kotlin
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(paddingState.value)
)
= remember {};这个状态的取值,必须使用.value
- 使用 by 委托语法
kotlin
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(padding)
)
* AS 有时会报 Type 'MutableState<Int>' has no method 'setValue ...
* 是因为 AS import 无法自动识别 by 关键字(属性委托)所需的扩展函数
* 手动添加导入:
* import androidx.compose.runtime.getValue
* import androidx.compose.runtime.setValue
使用委托语法后,取值时 直接使用状态变量即可
优化状态读取
看下面两段代码:
Text(Modifier.offset(state))
Text(Modifier.offset {
IntOffset(state)
})
- Modifier.offset(state),这种是 直接赋值的形式,在组合阶段就完成了赋值的读取。所以 state的变化,会直接影响组合阶段。
- Modifier.offset { } 这种是 使用了 lambda 代码块的形式。offset的作用,是会影响放置的位置,也可能会影响自身组件的大小。所以offset 中的状态应该在 布局阶段去读取。 compose 要求,使用 lambda {} 后,才会影响与推迟 状态读取的阶段。
当然,通常情况下,我们是在组合阶段读取状态。但适当情形下,减少重组、重布局,会提升性能。
优化的方式:
- 在 本意为 布局或 绘制阶段的方法中,采用 lambda {} ,来推迟 状态读取的阶段
- 在 某些场景中,使用 derivedStateOf 进行状态转换、过滤
- 自定义状态,封装一系列的子状态
分析如下代码,
kotlin
@composable
fun ShowUI() {
val state1 = remember {...}
val state2 = remember {...}
val state3 = remember {...}
ShowOtherUI(state1, state2, state3)
... // 比如在某些 UI点击回调中,修改了 state1/2/3 的值
}
@composable
fun ShowOtherUI(state1, state2, state3) {
...
}
当state1/2/3 分别有变化时,分别都会触发一次 关联的 ShowOtherUI?实际上在同一个事件回调里,同时修改了 state1/2/3,compose 会智能的 只触发一次重组。
- 注意,这里状态的声明用的是
= remember,前面说过,这种形式,需要通过state.value的形式才能读取值;而只有读取了state,才会触发重新执行 某个阶段。上面 向 ShowOtherUI 传递的是 引用,而不是值,因为没有读取值 也就不会触发 ShowUI 重组。- 而如果声明形式是
by remember,ShowOtherUI(state1, state2, state3),就是值传递了。那后面 state1/2/3 的变化,也会使得 ShowUI 发生重组
若是还有 n个状态需要维护,并参与到后续UI逻辑中。那为了避免 composable的代码过于复杂,就可以 自定义一个 状态,它封装了这一系列的子状态。
自定义状态示例:
kotlin
// 自定义状态
class MyCustomState {
// 内部状态
var state1 by mutableStateOf(false)
private set // 限制外部只能读,不能直接改
var state2 by mutableStateOf(0)
private set // 限制外部只能读,不能直接改
var state3 by mutableStateOf("")
private set // 限制外部只能读,不能直接改
fun m1() { // state1 的修改 }
fun m2() { // state2 的修改 }
fun m3() { // state3 的修改 }
}
// 提供专门的 remember 方法
@Composable
fun rememberMyCustomState(): MyCustomState {
return remember { MyCustomState() }
}
@composable
fun ShowUI() {
val state = rememberMyCustomState()
ShowOtherUI(state)
... // 比如在某些 UI点击回调中,修改了 state内部多个子状态 的值
}
@composable
fun ShowOtherUI(state: MyCustomState) {
...
}
重组循环 问题
kotlin
Box {
var imageHeightPx by remember { mutableIntStateOf(0) }
Image(
painter = painterResource(R.drawable.rectangle),
contentDescription = "I'm above the text",
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size ->
// Don't do this
imageHeightPx = size.height
}
)
Text(
text = "I'm below the image",
modifier = Modifier.padding(
top = with(LocalDensity.current) { imageHeightPx.toDp() }
)
)
}
imageHeightPx 在组合阶段,被 Text中的 Modifier.padding 所使用。在布局阶段,Image的 Modifer.onSizeChanged {} 回调中,根据 图片的 size 修改了 imageHeightPx 状态值。当前帧执行到这时,已经在布局阶段,无法回退成组合状态;需要再启一帧 重新执行组合阶段。
以 imageHeightPx 值的变化说明:
初始时 imageHeightPx 值为0,
第一帧内:组合阶段,Text 中的padding top 先读取了状态值,此时 还是 0;布局阶段,imageHeightPx 被赋值为图片 实际高度。由于组合阶段有读取状态,导致重组。
第二帧内:组合阶段,Text 中的padding top 读到了 图片的实际高度值;布局阶段,imageHeightPx 再次被赋值为图片 实际高度,前后值无变化,不再触发重组
此示例的问题在于,代码没有在单个帧中达到"最终"布局。该代码依赖发生多个帧,它会执行不必要的工作,并导致界面在用户屏幕上跳动。
代码中 "用布局阶段的结果(尺寸),去驱动组合阶段的参数(修饰符)",这必然导致一帧的延迟。
该示例的意图 就是纵向 布局 Image、Text;去除 imageHeightPx 的使用,简单使用 Column 就能达到该意图。
或使用 自定义布局 实现:
kotlin
@Composable
fun CustomLayout() {
// 不需要任何额外的 State
Layout(
content = {
// 定义两个需要被测量和放置的组件
Image(
painter = painterResource(R.drawable.rectangle),
contentDescription = "I'm above the text",
modifier = Modifier.fillMaxWidth()
)
Text(
text = "I'm below the image"
)
}
) { measurables, constraints ->
// 这里的代码全部运行在"布局阶段 (Layout Phase)"
// 1. 测量 Image (measurables[0])
val imagePlaceable = measurables[0].measure(constraints)
// 2. 测量 Text (measurables[1])
val textPlaceable = measurables[1].measure(constraints)
// 3. 计算父容器的总尺寸
val layoutWidth = maxOf(imagePlaceable.width, textPlaceable.width)
val layoutHeight = imagePlaceable.height + textPlaceable.height
// 4. 放置子元素
layout(width = layoutWidth, height = layoutHeight) {
// 图片放在顶部 (0, 0)
imagePlaceable.placeRelative(x = 0, y = 0)
// 文本紧挨着图片下方放置,y 轴坐标就是图片的高度
textPlaceable.placeRelative(x = 0, y = imagePlaceable.height)
}
}
}
遵循了单向数据流原则:在原错误代码中,数据的流动是 布局阶段(获取尺寸) -> 更新State -> 触发新一轮组合阶段(读取State)。这逆转了 Compose 组合 -> 布局 -> 绘制 的标准顺序。而在自定义 Layout 中,直接在布局阶段获取 imagePlaceable.height,并立即把它用作 textPlaceable 的 y 轴坐标。数据流动完全限制在"布局阶段"内部。