Android Compose 渲染 UI 帧的三个阶段:组合、布局、绘制

文章目录

UI帧的3个阶段

  1. 组合(Composition): 决定显示什么(执行 @Composable 函数,构建 UI 树)。
  2. 布局(Layout): 决定放在哪里(包含 测量-Measure和放置-Placement 两个子步骤)。
  3. 绘制(Drawing): 决定如何渲染绘制。
  • 在组合阶段,Compose 运行时会执行可组合函数,并输出表示界面的树结构 。此界面树由包含后续阶段所需的所有信息的布局节点组成
  • 在布局阶段,系统会使用以下三步算法遍历树:
  1. 测量子项:节点会测量其子项(如果有)。
  2. 确定自己的尺寸:节点根据这些测量结果确定自己的尺寸。
  3. 放置子节点:每个子节点都相对于节点自身的位置放置。
    在此阶段结束时,每个布局节点都具有:
    分配的宽度和高度;应绘制到的 x、y 坐标
  • 绘制阶段,系统会再次从上到下遍历UI树,并且每个节点依次在屏幕上绘制自身

状态读取

在 Compose 中,状态(State)在哪里被读取,决定了当状态改变时,Compose 会从哪个阶段开始重新执行。

  • 在组合阶段,读取到状态改变,即发生重组。完整重新执行 三个阶段:组合、布局、绘制。

简单理解组合阶段的状态:在 composable中,定义的 一些 remember 状态,它们一进入组合(或构建UI树时)就会执行,不在 布局和绘制阶段才执行

  • 在布局阶段,读取到状态改变,跳过组合,重新执行 两个阶段:布局、绘制。

比如在 Modifier.offset {} 中读取了状态

  • 在绘制阶段,读取到状态改变,跳过组合、布局,重新执行 绘制 阶段。

比如在 Canvas { drawRect(color = state.value) } 或者 Modifier.drawBehind { ... } 内部读取了颜色状态。

状态读取的两种访问方式

  1. 使用 = 进行状态赋值
kotlin 复制代码
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

= remember {};这个状态的取值,必须使用 .value

  1. 使用 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 轴坐标。数据流动完全限制在"布局阶段"内部。

相关推荐
帅得不敢出门2 小时前
Android Studio同一个工程根据不同芯片平台加载不同的framework.jar及使用不同的代码
android·android studio·jar
xiangxiongfly9152 小时前
Android LeakCanary源码分析
android·leakcanary
黄林晴2 小时前
紧急预警!Android 17 定位权限大改,你的 App 要适配了
android
夏沫琅琊3 小时前
Android API 发送短信技术文档
android·kotlin
周周不一样3 小时前
Android基础笔记1
android·笔记·gitee
取码网3 小时前
影视APP源码 SK影视 安卓+苹果双端APP 反编译详细视频教程+源码
android
musk12123 小时前
android webview 黑屏问题 , 页面加载时间有点长的情况下
android
夏沫琅琊3 小时前
Android 彩信导出技术文档
android·kotlin