当前位置: 首页 > news >正文

Compose 基础 - 重组优化

参考文章

一、概念

Compose 渲染三阶段:组合、布局、绘制。状态读取发生在哪一个阶段,重组就会从该阶段开始,重新执行该阶段及其之后的阶段。

1.1 强制跳过模式(Strong Skipping Mode)

参考文章

不稳定类型无法自动进行跟踪和比较,无论内容较上次有没有变化,强制重组以确保渲染的正确,功能正确性比性能更重要。

但在编码时,去更改不稳定类型的状态可能只是少数情况,为了跳过这不必要的重组,我们需要付出更多的成本(弄懂稳定类型、评估@Stable的添加、对跨Module或第三方库进行适配)。

自 Compose v1.5.4 引入强制跳过模式(Strong Skipping Mode),放宽了跳过重组的限制,让参数中的不稳定类型也能参与比较。稳定类型依旧使用 Object.equals() 比较,不稳定类型与 Composition 存储的历史值做 === 比较。Lambda 如果捕获了不稳定类型,依然可以被记忆。

相当于弱化了不稳定类型和稳定类型的区别,不稳定类型使用 === 比较,这里会存在风险(那些容器对象)。如果我们想要使用值比较替代地址比较,还是要依靠 @Stable 。对于那些在第三方库中无法添加注解的类型,可以使用稳定性配置文件(stability configuration files),详见。

1.1.1 开启方式

Kotlin 2.0.2 之后,强跳模式会默认开启。

composeCompiler { enableStrongSkippingMode = true }

1.1.2 带来的副作用

上面说去更改不稳定类型的状态是少数情况,但还是有的。

例如使用了可变集合 MutableList。当点击 Button 时 list 的内容发生变化,但由于不稳定类型采用 === 比较的是引用,集合实例没变此时会返回 true 使得接收该参数的可组合项跳过了重组,容器内元素的变化无法得到正确刷新。使用 mutableStateListOf() 创建可观察的 List,或者重新赋值。

另一方面,不稳定类型通过 === 作比较后返回 false 也不一定值不相等。通常情况下我们不会对 mutable 类型使用拷贝构造出多个相同值的实例,因此大多数情况下,实例不同即代表值不同,即便错杀了一两个无辜的 case 也无伤大雅。

1.1.3 对于 Lambda 传参

Lambda 默认是稳定类型,但捕获了外部不稳定类型(如ViewModel)的 Lambda 会被视为不稳定类型。

不捕获任何外部变量会为 Lambda 创建一个静态单例。
捕获了外部稳定类型Compose 编译器会自动为其包装一个 remember ,避免 Lambda 因重新创建实例而在重组中 diff 失败。这很重要,试想一下很多组件都有 Lambda 回调(例如 onClick 等),如果 Lambda 不被记忆,意味着大部分都无法跳过重组。
捕获了不稳定类型强跳模式开启下,不稳定类型的 Lambda 一样都会被包装 remember,捕获的外部变量 ViewModel 变为 remember 的 key。 Key 采用同样的比较规则,即不稳定类型 === 地址比较,稳定类型 Object.equals() 值比较。
//之前 onClick = { viewModel.onClicked() } //强跳 onClick = remember(viewModel){ viewModel.onClicked() }

二、组件读取状态

不管组件通过参数读取还是在函数体内读取,此时处于组合阶段,作用域是 RecomposeScope,只要状态更新就会重新执行完整的三个阶段。

2.1 内联组件的重组作用域与其调用者相同

一般情况下,读取某个状态的组件和未读取的组件,它们的重组作用域是隔离的互不影响。但是内联组件除外(Column、Row、Box等),由于 inline 函数的特性,内部所有子元素都会在编译期插入到外层中,所以 Column 内部组件读取状态的行为实际是发生在它的外层中(即重组作用域被放大),因此子元素重组会引发不必要的父容器重组。

@Composable fun Outer() { Column { //这里读取状态,实际是在Outer读取 } }

2.1.1 隔离重组作用域(采用非inline组合项)

当业务代码已经使用了大量的 Column 、Row、Box,就需要隔离重组作用域,将 inline 换成非 inline。具体很简单,创建一个自定义可组合函数包装一下,将之前的内容传入就行,然后用自定义的组件去替换内联组件。

@Composable fun Outer() { Inner() //替换 } @Composable fun Inner() { Column { //这里读取状态就是在Inner内了 } }

2.2 参数值在组合阶段就要被计算出来

子组件声明的参数,在调用它时就会被计算出来,即组合阶段就进行了状态读取,等同于在父容器中读取了状态再传给子元素,当状态变化时会也会造成父容器重组,扩大了重组作用域。

@Composable fun Outer( viewModel: DemoViewModel = viewModels() ) { //以下两种方式都是外部直接读取后传给子组合项 var data = viewModel.demoUiState.data Inner1(data = data) Inner1(data = viewModel.demoUiState.data) } @Composable private fun Inner1(data: String) {...}

2.1.1 使用 Lambda 往子元素中传参

将组件参数的类型替换成 Lambda(函数类型,也就是回调),子元素在函数体里能决定什么时候调用它,简而言之就是尽可能将读取状态的行为延后到真正使用它的时候。

@Composable fun Outer( viewModel: DemoViewModel = viewModels() ) { Inner(data = { viewModel.demoUiState.data }) } @Composable private fun Inner2(data: () -> String) {...}

2.1.2 优化 Lambda 传参使用函数引用

可忽略,详见强跳模式

在 Compose 中 Lambda 传参被用来实现回调控制(将事件处理交给调用处实现),但在 Kotlin 中 Lambda 实际会被编译成一个匿名内部类对象,由于 Compose 编译器会检查数据类型的稳定性,Lambda 默认是稳定类型,如果在 Lambda 中捕获了外部不稳定类型(如ViewModel),就会被视为不稳定类型导致组件重组。

@Composable fun Outer( val viewModel: DemoViewModel = viewModels() ){ Inner(onValueChange = viewModel::onValueChange) }

2.1.3 避免每次重组为 Lambda 生成匿名对象

每次重组都会为 Lambda 生成匿名对象,可通过 remember 持久化对象减少创建。

var count by remember { mutableIntStateOf(0) } val onClick = remember { { count++ } } Button({onClick()}) { Text("累加$count") }

2.3 子元素读取父容器外部状态,会导致其它带参的子元素重组

父容器都会向外暴露一个 @Composable Lambda 的 content 参数,一般情况下 content 中任何子元素都不会因为父容器的重组而发生重组,如果有子元素读取了父容器之外的状态,它的重组会除了引发父容器重组,还会引发整个 content 中其它带参的子元素重组。

@Composable fun Demo() { var counter by remember { mutableStateOf(0) } Button({ counter ++ }) { //文字1读取了父容器Button外部的状态counter,文字2未读取 //counter的变化会导致文字2的背景色也会变 Text(text = "Text1:$counter", modifier = Modifier.background(getRandomColor())) Text(text = "Text2", modifier = Modifier.background(getRandomColor())) } } fun getRandomColor() = Color( red = Random.nextInt(256), green = Random.nextInt(256), blue = Random.nextInt(256), alpha = 255 )

2.3.1 将其它带参子元素用无参可组合函数包装

@Composable fun Demo() { var counter by remember { mutableStateOf(0) } Button({ counter ++ }) { Text(text = "Text1:$counter", modifier = Modifier.background(getRandomColor())) TextWraper() } } @Composable fun TextWraper() { Text(text = "Text2", modifier = Modifier.background(getRandomColor())) }

2.3.2 缩小重组作用域

状态应与使用它的组件在同一重组作用域内,避免“父作用域状态被子作用域大量依赖”导致的大范围重组。如果一个状态是某个子元素专用的,不要提升到父容器中,而是限定在自己内部,避免重组作用域扩大。

@Composable fun Container() { Column { Counter() //独立的重组作用域 Other() //不重组 } } @Composable fun Counter() { var count by remember { mutableIntStateOf(0) } Button({count++}) { Text("自增$count") } }

三、Modifier读取状态

Modifier 分为布局类型、绘制类型、交互类型,参与的是布局阶段和绘制阶段,理应跳过不必要的组合阶段。标准的 Modifier 函数是一定会在组合阶段被执行的,当状态变化时会重新创建 Modifier 实例,UI树会先删除旧的再添加新的实例,而UI树的变化会导致重组,每次重组都可能触发 组合→布局→绘制 三个阶段,若读取的是一个频繁变化的状态非常要命(动画或滚动)。

布局阶段读取状态测量时读取状态:在 Layout() 的 measure 闭包中读取状态,会跳过组合阶段直接从该节点的测量开始。
放置时读取状态:布局类型如 Modifier.offset{},这个 Lambda 会在布局阶段的放置时才被调用,作用域是 PlacementScope,状态更新时会跳过组合阶段,由于大小没变,在布局阶段甚至不会再次测量。
绘制阶段读取状态绘制类型如 Modifier.drawBehind{},这个 Lambda 会在绘制阶段才被调用,作用域是 DrawScope,状态更新时会跳过组合阶段和布局阶段,不需要 CPU 的参与直接调用 GPU 进行重绘。
//对背景色应用动画,Box必须在每一帧上重组,因为每一帧的颜色值都在变化 var color by animateColorAsState(...) Box(Modifier.background(color))

3.1 使用带 Lambda 参数版本的 Modifier

有时不可避免需要一直读取一个不断变化的值(offset、size、background、padding)从而达到变化的绘制效果。带 Lambda 参数版本的 Modifier 函数不会在组合阶段被执行,根据修改类型延迟到布局阶段或绘制阶段,这样Modifier实例不会改变,因此UI树不会变化,Compose只会在需要的时候调用 Lambda,也就可以跳过不必要的重组了。

var offset by remember { mutableStateOf(IntOffset(10, 10)) } Box( modifier = Modifier .offset(offset.x.dp, offset.y.dp) //直接读取 .offset{ offset } //在Lambda中读取 )

3.2 多个绘制类型使用 graphicsLayer() 合并

绘制类型的修饰符(alpha、rotate、clip)底层都是使用 Modifier.graphicsLayer() 实现的,都会创建一个图形层,相当于在反复支付这个“图层税”。使用 Modifier.graphicsLayer() 相当于批处理,一次性完成多种图形变换,减少GPU过度绘制避免重复计算。由于只是绘制,当它读取状态时,请求的是 GPU 重绘,完全绕过了 Kotlin 代码的执行逻辑。这几乎是 0 CPU 开销

Modifier.graphicsLayer { alpha = 0.5F, shadowElevation = 8.dp.toPx(), shape = RoundedCornerShape(12.dp), clip = true, renderEffect = BlurEffect(10f, 10f) }

3.3 提取 Modifier 重复使用

在可组合项中观察频繁变化的状态(如动画或滚动)时,可能会发生大量重组。在这种情况下,Modifier 会在每次重组时获得分配,并可能分配给每一帧。可以提取 Modifier 到可组合项外面来重用。

val myModifier = Modifier.size(10.dp) //分配在这里进行 @Composable fun LoadingScreen() { val animatedState = animateFloatAsState(/*...*/) LoadingWheel( // modifier = Modifier.size(10.dp), //Modifier的创建和分配将在动画的每一帧中进行! modifier = myModifier, //此处不会分配,因为只是重复使用同一实例 animateState = anianimateState ) }

四、列表设置key

Compose编译器会为每个可组合函数生成 slotId 用于标记组件在UI树中的位置。一般情况下组件在代码中的位置是不会变的,但是使用列表的情况除外。当生成一个列表时,对Compose 来说,分配key是很困难的。

4.1 循环生成子元素

以下代码每次都从相同位置调用 Item(data),每个 Item 表示列表中不同的条目,因此是UI树上的不同节点,在这种情况下 Compose 依赖于调用顺序生成唯一的 key 并仍能够区分它们。

当将一个新元素插入到列表尾部时,这段代码仍能正常工作,因为其余的调用保持在相同的位置。但若将一个新元素插入到列表头部或者中间时,Compose runtime 会重组该元素位置后面所有的Item,因为它们改变了位置,即便内容没有发生变化,这是非常低效的而且本应该跳过重组。

Compose提供了 key() 函数用来设置唯一标识符,来感知 Item 的位移、新增、删除( 对于内容是否变化能自动通过元素对象自身的equals来感知)以便跳过重组。

@Composable fun Demo() { Column { for(data in datas) { // key(data.id) { //使用唯一值当作key Item(data) // } } } }

现象:点击按钮从列表头部插入新元素,结果打印了最后一个元素,初始化的4个元素都重组了一次。

原因:调用 forEach() 遍历时,生成4个 Item(子组合项)并依次设置 Element(列表元素),LaunchEffect() 也就输出了4次,但是由于 Item 和 Element 没有一一对应,当列表头部新插入一个 Element 时,会把新 Element 设置到第一个 Item 上,其余 Element 依次设置到后面的 Item 上,最后一个 Element 设置到新生成的 Item 上,因此前四个 Item 发生了重组而最后一个是新增的就不存在重组了。

@Composable fun Demo() { val list = remember { mutableStateListOf<DataBean>().apply { for(i in 0 until 5) { add(DataBean(i)) } } } Column { Button(onClick = { list.add(0, DataBean(0)) }) { Text(text = "从头插入新元素") } list.forEach { element -> // key(element.id) { LaunchedEffect(key1 = Unit) { Log.e("添加元素", "元素ID:${element.id}") } Text(text = "元素ID:${element.id}") // } } } }

4.2 LazyList

items( items = dataList, key = { it.id } //it是dataBean ) {...}

五、使用 derivedStateOf() 派生低频状态

一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。对条件状态进行过滤,避免每次条件状态更新都要连带自己重组(将高频变化的状态转换成低频变化的状态)。通常使用remember的key可以实现(当UI刷新频率和key一致时使用),有些情况的状态无法用作key,例如 Element 改变了而 List 没变。

//每次重组遍历titles看是否包含关键字的标题,非常耗性能 //只在每次更新titles的时候去遍历过滤出包含关键字的标题 @Composable fun Demo( keywords:List<String> = listOf("关键字1", "关键字2", "关键字3") ) { val titles = remember { mutableStateListOf<String>() } val result = remember(keywords) { derivedStateOf { titles.filter { keywords.contains(it) } } } LazyColumn(modifier = Modifier.fillMaxWidth()) { items(result) { ... } //包含关键字的标题的列表 items(titles) { ... } //全部标题的列表 } }
@Composable fun Demo() { val list = remember { mutableStateListOf<String>() } val showText by remember { derivedStateOf{ list.size.toString() } } }

六、避免打印日志造成重组

读取状态会造成重组,有时候需要日志打印状态值,可以使用附带效应API,还可以通过扩展函数简化调用。

@Composable fun Demo() { var listState = rememberLazyListState() //方式一 SideEffect { Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}") } //方式二 TAG.printLog { "List recompose ${listState.firstVisibleItemIndex}" } } @Composable fun String.printLog(block: () -> String) { SideEffect { Log.d(this, block()) } }

七、避免反向写入(Avoid backwards write)

在读取了状态之后,在可组合项中又更新了状态,会因为之前读取的状态过期又重新读取导致重组,如此无限循环。

@Composable fun Demo() { var count by remember { mutableStateOf(0) } Button( onClick = { count++ }, //正确写入方式 Modifier.wrapContentSize() ) { Text("点击触发重组") } Text("$count") count++ //错误写入方式:此处反向写入 }
http://www.jsqmd.com/news/471559/

相关文章:

  • 基于麻雀算法深度优化极限学习机的故障诊断方法(SSA-DELM)
  • 2026年南昌口碑好的养老服务中心盘点,赣洪仁心医养值得关注 - 工业推荐榜
  • Redis的常用功能
  • 响应“双碳”战略:2026西北五大合规再生资源回收服务商榜单发布 - 深度智识库
  • 电感/变压器/二极管的降额使用案例实战B01
  • 溪水潺潺,峰林相望:这才是张家界的慢时光
  • 第4天 蜜罐堡垒机api
  • MyPal3(8)
  • 探索Qt + OpenCV视觉通用框架:从原理到代码实践
  • 基于深度置信网络DBN的数据分类预测
  • 穿梭车货架性价比高的品牌有哪些,苏弘智能仓储在列吗 - 工业品牌热点
  • 集成房屋来图定制费用多少,宁波靠谱的实力厂商有哪些 - myqiye
  • 脉振方波高频注入法与 SMO 观测器模型复合控制:探索电机控制新境界
  • 免编程拖拽C#源码:工控软件开发的新启发
  • 为什么现在需要卸载OpenClaw:它对你的系统安全做了什么?
  • 杉德斯玛特卡回收方法对比:哪家最安全靠谱? - 团团收购物卡回收
  • 计算机毕业设计springboot电子书包之用户与权限管理组件 基于SpringBoot的智慧校园电子书包身份认证与访问控制平台 基于SpringBoot的数字化教学环境用户权限管控与资源保护系统
  • 2026年显微镜价格大汇总,科研用显微镜性价比高的品牌推荐 - 工业品网
  • 【会员专享数据】2025年全国省份点位数据(shp格式\excel格式)
  • 2026采购指南:如何根据您的工业场景选择最合适的防爆巡检机器人品牌? - 品牌推荐大师
  • 2026功能实用智能客服机器人推荐,实用性强的人工智能客服优选 - 品牌2026
  • Android中横屏适配以及各个屏幕之间的适配
  • 计算机毕业设计springboot社区养老服务系统的设计与开发 基于SpringBoot的社区智慧养老服务平台的设计与实现 基于Java的社区居家养老综合服务管理系统的设计与开发
  • 2026年口碑不错的DLC涂层机生产厂家,性价比高的有哪些品牌 - 工业设备
  • FireRedASR-AED-L WebUI界面自定义与功能扩展效果展示
  • 2026 年全国透光混凝土优质厂商盘点 全场景项目选型实用参考指南 - 深度智识库
  • OpenClaw 腾讯云 (大龙虾AI)保姆级教程
  • 基于分布式优化的多产消者非合作博弈能量共享:MATLAB 实现探索
  • MySQL中的视图
  • 2026年通风系统核心部件推荐:从风阀看西北市场的“硬实力”制造商 - 深度智识库