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

张开发
2026/4/3 18:56:17 15 分钟阅读
Android Compose 渲染 UI 帧的三个阶段:组合、布局、绘制
文章目录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 { … } 内部读取了颜色状态。状态读取的两种访问方式使用 进行状态赋值valpaddingState:MutableStateDpremember{mutableStateOf(8.dp)}Text(textHello,modifierModifier.padding(paddingState.value)) remember {}这个状态的取值必须使用.value使用 by 委托语法varpadding:Dpbyremember{mutableStateOf(8.dp)}Text(textHello,modifierModifier.padding(padding))*AS 有时会报 Type MutableStateInt has no method setValue...*是因为 ASimport无法自动识别by关键字属性委托所需的扩展函数*手动添加导入:*importandroidx.compose.runtime.getValue*importandroidx.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 进行状态转换、过滤自定义状态封装一系列的子状态分析如下代码composablefunShowUI(){valstate1remember{...}valstate2remember{...}valstate3remember{...}ShowOtherUI(state1,state2,state3)...// 比如在某些 UI点击回调中修改了 state1/2/3 的值}composablefunShowOtherUI(state1,state2,state3){...}当state1/2/3 分别有变化时分别都会触发一次 关联的 ShowOtherUI实际上在同一个事件回调里同时修改了 state1/2/3compose 会智能的 只触发一次重组。注意这里状态的声明用的是 remember前面说过这种形式需要通过state.value的形式才能读取值而只有读取了state才会触发重新执行 某个阶段。上面 向 ShowOtherUI 传递的是 引用而不是值因为没有读取值 也就不会触发 ShowUI 重组。而如果声明形式是by rememberShowOtherUI(state1, state2, state3)就是值传递了。那后面 state1/2/3 的变化也会使得 ShowUI 发生重组若是还有 n个状态需要维护并参与到后续UI逻辑中。那为了避免 composable的代码过于复杂就可以 自定义一个 状态它封装了这一系列的子状态。自定义状态示例// 自定义状态classMyCustomState{// 内部状态varstate1bymutableStateOf(false)privateset// 限制外部只能读不能直接改varstate2bymutableStateOf(0)privateset// 限制外部只能读不能直接改varstate3bymutableStateOf()privateset// 限制外部只能读不能直接改funm1(){// state1 的修改 }funm2(){// state2 的修改 }funm3(){// state3 的修改 }}// 提供专门的 remember 方法ComposablefunrememberMyCustomState():MyCustomState{returnremember{MyCustomState()}}composablefunShowUI(){valstaterememberMyCustomState()ShowOtherUI(state)...// 比如在某些 UI点击回调中修改了 state内部多个子状态 的值}composablefunShowOtherUI(state:MyCustomState){...}重组循环 问题Box{varimageHeightPxbyremember{mutableIntStateOf(0)}Image(painterpainterResource(R.drawable.rectangle),contentDescriptionIm above the text,modifierModifier.fillMaxWidth().onSizeChanged{size-// Dont do thisimageHeightPxsize.height})Text(textIm below the image,modifierModifier.padding(topwith(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 就能达到该意图。或使用 自定义布局 实现ComposablefunCustomLayout(){// 不需要任何额外的 StateLayout(content{// 定义两个需要被测量和放置的组件Image(painterpainterResource(R.drawable.rectangle),contentDescriptionIm above the text,modifierModifier.fillMaxWidth())Text(textIm below the image)}){measurables,constraints-// 这里的代码全部运行在“布局阶段 (Layout Phase)”// 1. 测量 Image (measurables[0])valimagePlaceablemeasurables[0].measure(constraints)// 2. 测量 Text (measurables[1])valtextPlaceablemeasurables[1].measure(constraints)// 3. 计算父容器的总尺寸vallayoutWidthmaxOf(imagePlaceable.width,textPlaceable.width)vallayoutHeightimagePlaceable.heighttextPlaceable.height// 4. 放置子元素layout(widthlayoutWidth,heightlayoutHeight){// 图片放在顶部 (0, 0)imagePlaceable.placeRelative(x0,y0)// 文本紧挨着图片下方放置y 轴坐标就是图片的高度textPlaceable.placeRelative(x0,yimagePlaceable.height)}}}遵循了单向数据流原则在原错误代码中数据的流动是 布局阶段(获取尺寸) - 更新State - 触发新一轮组合阶段(读取State)。这逆转了 Compose 组合 - 布局 - 绘制 的标准顺序。而在自定义 Layout 中直接在布局阶段获取 imagePlaceable.height并立即把它用作 textPlaceable 的 y 轴坐标。数据流动完全限制在“布局阶段”内部。

更多文章