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

相关推荐
哑巴湖小水怪13 小时前
Android的架构是四层还是五层
android·架构
2501_9160088915 小时前
深入解析iOS应用启动性能优化策略与实践
android·ios·性能优化·小程序·uni-app·cocoa·iphone
美狐美颜SDK开放平台16 小时前
短视频/直播双场景美颜SDK开发方案:接入、功能、架构详解
android·ios·美颜sdk·第三方美颜sdk·视频美颜sdk
untE EADO17 小时前
在 MySQL 中使用 `REPLACE` 函数
android·数据库·mysql
iblade17 小时前
Android CLI And Skills 3x faster
android
阿巴斯甜19 小时前
SharedUnPeekLiveData和UnPeekBus的区别:
android
阿巴斯甜19 小时前
UnPeek-LiveData的使用:
android
我就是马云飞19 小时前
我废了!大厂10年的我面了20家公司,面试官让我回去等通知!
android·前端·程序员
limuyang220 小时前
在 Android 上用上原生的 xxHash,性能直接拉满
android
Ulyanov21 小时前
《玩转QT Designer Studio:从设计到实战》 QT Designer Studio组件化开发与UI组件库构建
开发语言·python·qt·ui·雷达电子战系统仿真