布局与测量:让Compose从&

B站影视 内地电影 2025-10-29 04:15 2

摘要:Compose 虽然极大地简化了 UI 声明,但自由的组合也容易让我们不自觉地“套娃式”疯狂嵌套 Column 、Row 、Box 等等组件。写起来是爽了,但是这些布局叠加起来,在渲染阶段会产生额外的测量和绘制开销,最终演变成页面卡顿、帧率下降,仿佛一张张幻灯

“为什么我的 Compose 页面滑动时像幻灯片一样卡?因为布局嵌套正在谋杀你的性能!”

在上一篇中,我们揭开了 Compose 重组背后的真相,识别了那些看不见却致命的状态陷阱。本系列文章如下(正在更新中):

状态管理:Compose的隐形炸弹?从重组陷阱到性能救赎

我们或许已经优化了状态声明、控制了重组范围,结果却发现页面依旧滑动不畅、加载迟缓?别急,这一次的元凶,可能藏在 布局层级 里。

Compose 虽然极大地简化了 UI 声明,但自由的组合也容易让我们不自觉地“套娃式”疯狂嵌套 Column 、Row 、Box 等等组件。写起来是爽了,但是这些布局叠加起来,在渲染阶段会产生额外的测量和绘制开销,最终演变成 页面卡顿、帧率下降 ,仿佛一张张幻灯片在切换。

本篇将聚焦于另一个常被忽视的性能黑洞—— 布局 #技术分享与测量 。带领我们从“嵌套地狱”走向“扁平化管理”,剖析 Compose 的布局原理,掌握识别性能瓶颈的方法,并奉上一些实战优化策略,让我们的 Compose 页面丝滑如初。

开始之前,笔者先简单聊一聊 Compose 的布局与测量机制。我们都知道,Compose 作为近几年推出的声明式 UI 框架,它摒弃了传统 XML + imperative layout 的模式,转而采用函数式组合和响应式 UI 构建方式。追源溯根,这套创新机制的背后,Compose 的布局系统依旧遵循一套”类似工厂流水线“的机制。

在学习的过程中,是不是经常会在网上听到这样一个形象的说法,“Compose 的布局就像一条工厂流水线:先测量尺寸 → 再决定位置 → 最后绘制出来” ,这个比喻对于我们初学者来说还是比较友好的。

测量:计算下零件的具体尺寸

在这个阶段中,确定子组件的尺寸,在给定父组件约束的前提下,计算出每个子项“允许使用”的宽高。怎么说呢,我们简单看下 Compose 内部的代码,不难发现,我们创建的每个布局组件都会接收一组约束( Constraints ),包括:

最小宽度( minWidth )和最大宽度( maxWidth )最小高度( minHeight )和最大高度( maxHeight )

然后,布局组件会对每个子项调用

val placeable = measurable.measure(constraints)

这里得到子项的 Placeable 对象代表了最终已经被测量好的组件,这里面就包含了最终得到的宽高。

ps:需要注意的点

Compose的测量是从上到下递归进行的

每个子项必须被测量一次,才能进入到下一个阶段

某些布局(比如说 Comlun )可能需要测量其中包含的所有子项后,才知道自身应该有多高

如果说”测量“是量尺寸多大,那么”放置“就是进行搬运和安装,既然我们已经知道了每个零件(子组件)有多大,接下来就要把它们装到指定的位置,就像给每个螺丝、板材和电路板分配好精确的位置。

我们还是从源码的角度出发,在 Compose 中,这个阶段主要是调用每个子项的 place 或者 placeRelative 方法,把它们按照一定规则放入父组件的坐标系中。通常在 layout 代码块里完成的,比如说下面这样:

layout(width, height) {placeable.placeRelative(x, y)}

这里的 x 和 y 就是子组件相对于父组件的放置位置。如果用的是 Box 、Column 等组件,它们内部其实也是在做这样的放置,只不过根据我们外部设置的 Alignment 来决定坐标而已。

另外需要注意的是:

放置必须 基于测量结果 ,不能放置一个还没被测量的组件放置是 从父组件到子组件 执行的,与测量的“自上而下”流程一致放置的结果不会影响尺寸,尺寸在测量阶段就已经决定好了,只影响显示的位置绘制:把这些零件进行组装成完整的机器,刷漆染色

​ 当所有组件都“就位”之后,就轮到最后一步了:涂色、雕刻、打光,合成——其实也就是“绘制”。在这个阶段,Compose 会遍历 Layout Tree ,把每个节点的内容通过 Canvas 画到屏幕上,我们就可以看到最终呈现出来的 UI 效果啦

​ 如果我们使用了 Modifier.drawBehind 、Canvas 、drawWithContent 等等绘图渲染,它们就会在这个阶段生效,在这个过程中不会再做测量或放置了,它只关心:“我要把什么画在哪里、画成什么样”

​ 绘制是最终可视化输出阶段,不影响布局逻辑;它在 Compose 的渲染管线中处于最后一步,但也会受到上层尺寸和位置的影响

随着我们深入理解 Compose 的底层机制,就会发现流水线的说法其实并不准确,所以笔者前文说的是类似流水线机制,只是方便大家理解;我们可以从以下几个方面看看,因为本篇文章重点不在这里,如果想要深入了解的小伙伴可以去探究学习,这里笔者就不过多赘述了,从以下几个方面来简单提一嘴:

Compose 的布局是”树“,不是”线“

在实际的流水线作业下,每道工序都是线性执行,处理完一个任务再处理下一个。但是 Compose 的布局其实是一颗树,必须先测量子项、再递归合成;放置子项时,也要层层嵌套。就好像在装修一样,师傅同时在不同的房间进行测量,放置家具,最终在一起弄成整体的风格效果。

测量和放置是“一体”的,不存在“先测完再统一放”

这个我们在实际开发中应该深有体会,在定义 MeasurePolicy 时,测量和放置都是写在一起的 ,其实并不是所谓的先测量后放置,官方也说了布局=测量+放置,比如说我们常用的 Box 组件,其实你仔细看它的内部代码,也是这样的

measure(measurables, constraints) {val placeable = measurables[0].measure(constraints)layout(placeable.width, placeable.height) {placeable.place(0, 0)}}

这段代码里没有明显的“阶段线性”,不是按照顺序这样跑下来,而是我们一边测量,一边马上决定放哪。这就像我们不是“先把所有零件量完再去装箱”,而是“量完一个就装一个”。

Compose 是响应式,不是命令式的

传统 UI 开发是命令式的:“先测量,再布局,再绘制”,我们手动调用 API 安排各阶段。而 Compose 是响应式的,它根据状态自动决定 该不该测、测谁、怎么测 。怎么说呢,就像开车一样,传统 UI 就是自己亲自开车,油门刹车方向盘都得自己控制;而 Compose 就好比无人自动驾驶,只需要告诉它目的地,它就负责带你过去,路况刹车,加速都交给它自己判断,我们不需要操心。

所以说 Compose 其实并不是固定的线性流程,更符合一个动态调度系统。

​ 此外,我们还需要了解一下固有测量( intrinsic measurement ) 以及一些常见的组件的测量策略

简单点来说,Intrinsic Measurement 是指 组件在没有实际约束下的理想尺寸 。Compose 中提供了以下 API :

Modifier.width(IntrinsicSize.Min)Modifier.height(IntrinsicSize.Max)Min 表示子项“撑不开”时的最小尺寸;Max 表示内容“最多能撑多大”。

Box 组件相信大伙都不陌生了,经常会在 Compose 布局中用到,如果我们往 Box 里放多个子项,它会测量每一个,并把它们叠加放在左上角,其实就是类似于 FrameLayout 的行为。

Box 的策略非常简单粗暴:

它会将自己接收到的整个 Constraints 原封不动传递给所有子项 。不对子项进行大小限制,也不会主动裁剪。自身的尺寸通常就是所有子项中所需尺寸的“最大值”。val placeable = measurable.measure(constraints)

总结一下: Box 是“给多少用多少”,测量时几乎不做干涉。

这两个组件的测量策略比较复杂,但同样也很经典,我们可以把它们看成是“垂直/水平的队列布局容器”,它们的测量逻辑大致是:

垂直方向上: 将所有子项逐个测量后累加高度水平方向上: 取所有子项的最大宽度作为自身宽度会遍历每个子项,给它们设置“无限”高度以内的约束,让它们自由发挥。

伪代码如下所示:

var totalHeight = 0var maxWidth = 0for (child in measurables) { val placeable = child.measure(constraints) totalHeight += placeable.height maxWidth = max(maxWidth, placeable.width) }layout(maxWidth, totalHeight) { ... }

Row 也是类似逻辑,这里就不过多赘述了:

水平方向加总宽度,高度取最大;类似于横向铺砖。

总结一下: Column 是“叠罗汉”,高一直加,宽取最大; Row是“横向铺砖”,高度最大,宽度一直加

和普通 Column 不同,LazyColumn 并不会一次性测量所有子项。它的策略是:

只测量当前可见区域内需要显示的项(基于滚动位置)超出屏幕范围的项不会被测量也不会绘制 ,大大提升性能;它依赖 LazyListScope 来动态生成子项,因此“测量策略”也更灵活。

LazyRow 和 LazyColumn 基本一致,由于篇幅原因,这里就不过多说明了。

总结一下: LazyColumn 是“滑到哪里测哪里”,按需加载不浪费。

ConstraintLayout 的测量策略类似于 Android 传统 ConstraintLayout ,但也有适配 Compose 的特性,如下所示:

每个子项的测量顺序是由“约束关系图”决定的;会做“先测部分 → 约束分析 → 再精确测”的两轮测量;支持复杂依赖链,比如“子A宽度=子B宽度的两倍”等。

这就决定了它非常灵活但也比较复杂,适合用于动态、非规则 UI 布局。

总结一下: ConstraintLayout 灵活多变,适用于动态非规则布局。

这时候就有同学问了,不是说好的优化么,怎么还没进入主题,是标题党准备挨打嘛?别急别急,前面我们花了比较长的篇幅去了解 Compose 的布局与测量机制,目的就是为了更好得理解 Compose 的布局阶段出现的性能问题,从而更好的去优化它们。

OK,接下来我们正式进入主题了,中国人不骗中国人。

首先笔者先从几个方面简单总结下“为什么在 Compose 的布局测量阶段出现性能问题”

测量和布局要反复跑很多次 布局是个递归过程,父组件得先知道子组件大小,子组件又得测量,特别复杂时测量次数会翻倍。这样以来,视图加载的时候难免出现掉帧卡顿的现象。用固有测量(Intrinsic)会多测量几次

比如我们使用 IntrinsicSize.Min 的时候,它让系统先预估尺寸,再正式测量,等于测了两遍,时间自然而然不就多了嘛。

布局计算在主线程,卡顿直接影响体验

测量和布局都得主线程做,这样一旦测量和布局耗时较长,慢了界面就开始卡了。

内容变了要重新测量,频繁更新更费劲

内容一但发生了变化,整个测量流程又得重新跑一遍,在一些复杂布局下特别耗性能。

没限制尺寸,系统要算得更复杂

有时候我们的布局资源没有明确大小限制,系统要自己算出“理想大小”,想想都更费时间。

二、“嵌套地狱”的成因与表现套娃式布局的真实成本:不是你写得多,是它测得狠

很多同学一开始(包括笔者自己 ‍♂️)都会觉得,Compose 已经摆脱了传统 Android LinearLayout/RelativeLayout 那种深层嵌套的“祖传问题”,所以怎么嵌套都没关系,于是就开始随心所欲,但其实真相是:嵌套过深,性能照样会崩 ,区别在于只是换了形式而已。

Compose 底层是递归测量树,每增加一层嵌套,系统就要多一次完整的测量

​ 也就是说,写的每一层 Box、Column、Row、Surface ……哪怕只是为了“方便加个背景色”,都会消耗一次完整的测量+放置过程

如果当布局陷入“套娃式”堆叠时,问题往往不是立刻显现的,而是在 实际使用场景 里逐渐暴露出来:

滑动卡顿 列表里每个 item 都要递归测量 → 多层嵌套叠加,导致每一帧的计算压力骤增,滑动手势一快就开始掉帧了。过度重绘 层级过多时,哪怕只是某个小子元素状态发生改变,也可能导致父布局乃至整块区域被迫重新绘制。难以维护 代码层面看上去只是“多几层包装”,但当我们之后回头想改样式的时候,或者交给新人接手的时候,层层 Box/ Column 嵌套就像洋葱一样,一层一层拨开我的心,最后都是性能饱受摧残的眼泪,维护成本也相对比较高。

之前做过一个电商 APP,首页要渲染一堆商品卡片,而每个商品卡片需要展示:

上面是一张商品图片;图片右上角要加个「限时优惠」的角标;下面是一行标题,再下面是价格,右边还有一个收藏按钮。

OK,这很正常一个需求,这时候我们开始用 Compose 写代码,大致如下:

@Composablefun ProductCard(item: Product) {Surface(modifier = Modifier.fillMaxWidth.padding(8.dp),shape = RoundedCornerShape(8.dp),tonalElevation = 4.dp) {Column(modifier = Modifier.padding(12.dp)) {Box {Image(painter = rememberAsyncImagePainter(item.image),contentDescription = null,modifier = Modifier.fillMaxWidth.height(180.dp).clip(RoundedCornerShape(8.dp)))Box(modifier = Modifier.align(Alignment.TopStart).background(Color.Red, RoundedCornerShape(bottomEnd = 8.dp)).padding(4.dp)) {Text("限时优惠", color = Color.White)}}Spacer(Modifier.height(8.dp))Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth ) { Column { Text(item.title, maxLines = 1) Text("¥${item.price}", color = Color.Red) } IconButton(onClick = { }) { Icon(Icons.Default.FavoriteBorder, contentDescription = null) } } } } }外层先来个 Surface 包裹下,负责卡片的背景和阴影;内容用一个 Column ,上下堆叠;图片部分要加个角标,于是用 Box 包住 Image ,再在右上角叠一层小 Box 放角标文字;图片下面是间距 Spacer ;最后用一个 Row 放标题、价格和收藏按钮;标题和价格又用 Column 包起来,收藏按钮用 IconButton 。

非常好,仅仅一张商品卡片,就嵌套了8-10层布局。

那么问题来了,如果只有一个卡片的时候,我们可能完全感觉不到性能问题。但是当它出现在一个列表里的时候,情况就不一样了,滑动时,每一个商品卡片都要 递归测量 :Surface → Column → Box → Image … ,每层都要把约束传下去;有些容器(比如 Column )需要先测量所有子项,才能知道自己多高,这会导致 额外的重复测量 ;如果列表里有 50个甚至更多个商品卡片的话,实际就是 几百次甚至上千次测量 在同一帧发生。那么结果可想而知,滑动时明显掉帧,用户感受到一卡一卡的,影响体验。

这就是 嵌套地狱 的真实成本,不是我们写了多少代码,而是它 测得太狠了

Compose 的性能瓶颈,往往不是出在“代码写得多”,而是“嵌套层级太深”。别慌,下面我们就从几个角度通过一些案例,进行拆解优化,把复杂的嵌套“压平”,让布局更扁平、更轻量

比如现在做一个社交类 APP,首页要展示用户头像。设计师提了 3 个小要求:

圆形头像外边一圈描边和阴影背后加一个灰色背景

看起来需求很简单,很多同学第一反应就是:外面套个 Box 做背景 → 再套 Surface 做圆角和阴影 → 最里面放 Image 。三层嵌套,写起来很自然,UI 也对了。

Box(modifier = Modifier.size(64.dp).background(Color.Gray, CircleShape)) {Surface(shape = CircleShape,color = Color.Transparent,shadowElevation = 2.dp) {Image(painter = painterResource(R.drawable.avatar),contentDescription = null,modifier = Modifier.fillMaxSize)}}

但问题是——首页可能有几十、上百个头像一起渲染。每多一层嵌套,Compose 就要多一次 测量 + 布局 + 绘制 。这样算下来,本来一个头像只需要 1 次绘制,现在却变成了 3 次。整个列表就凭空多出几百次无效开销,滑动起来明显就会卡。

优化思路

现在我们换个思路想想,其实头像这种场景,根本不需要多层容器。Compose 的 Modifier 就像乐高积木:背景、圆角、描边、阴影 都能链式组合。换句话说,这些视觉效果本身不一定要“多套一层布局”来实现,而是可以直接写在 Modifier 上。

于是我们就得到优化版本:

Image(painter = painterResource(R.drawable.avatar),contentDescription = null,modifier = Modifier.size(64.dp).clip(CircleShape).border(2.dp, Color.Transparent, CircleShape).background(Color.Gray, CircleShape).shadow(2.dp, CircleShape))

这样写的好处:

视觉效果几乎一样层级少了 2~3 层,性能更轻量

假设列表里有 100 个头像,这种写法就能帮你直接减少 200~300 层无意义的布局 。对于性能敏感的首页来说,这就是 平白节省掉的开销

当然,现实开发中如果 UI 要求很严格,比如阴影效果必须是特殊形状,或者背景和前景需要分开处理,那还是要根据设计稿选择合适的容器。但绝大多数日常场景下,别一上来就“想要啥效果就多包一层”,先问问自己:这个效果能不能直接用 Modifier 组合拼出来?

此时进入聊天界面的时候,我们需要渲染这样的消息气泡:

左边是头像右边一列文字文字下面还有个时间戳

开始开发的时候,我的第一反应是:外层 Row → 放头像 & Column,Column 里再放 Text + 时间,再加 Spacer 调整间距。于是乎,写了如下的代码:

Row(Modifier.fillMaxWidth) {Image(painterResource(R.drawable.avatar), null, Modifier.size(40.dp))Spacer(Modifier.width(8.dp))Column {Text("你好,这是一条消息")Text("10:24", fontSize = 12.sp, color = Color.Gray)}}

看起来没啥问题是嘛,但别忘了这是在消息列表里,肯定不止这一条消息的。

之前我们也提到过,Row 和 Column 都是“二次测量”的容器:

Row 先测量所有子项的高度,再决定自己多高,Column 也是同理;像现在这样,一层 Row + 一层 Column,就意味着同一批子项被来回测量两遍。放在一个 50 条消息的列表里,这开销就成倍放大,这样能不卡嘛。 优化思路

既然这样,消息样式布局基本不会有太大改变,就是布局固定的话(头像永远在左,文字+时间永远在右),完全可以写一个 自定义 Layout ,一次测量+一次摆放,省掉中间的重复消耗。话不多说,开始优化下

@Composablefun MessageBubble(text: String, time: String,avatarImg: Int) {Layout(content = {Image(painterResource(avatarImg), null, Modifier.size(40.dp))Text(text)Text(time, fontSize = 12.sp, color = Color.Gray)}) { measurables, constraints ->val avatar = measurables[0].measure(constraints) val textM = measurables[1].measure(constraints) val timeM = measurables[2].measure(constraints)val height = maxOf(avatar.height, textM.height + timeM.height) layout(constraints.maxWidth, height) { avatar.place(0, (height - avatar.height) / 2) textM.place(avatar.width + 8, 0) timeM.place(avatar.width + 8, textM.height) } } }

Row + Column 两层 → 自定义 Layout 一层 ,对于高频场景(聊天、Feed 流),效果特别明显。

在电商 APP 中的商品卡片经常有「折扣角标」,但有些商品没打折就不需要显示,这里很多同学会直接在 Box 里写一个角标,然后用 if (hasDiscount) 决定显不显示。代码如下所示:

Box {Image(painterResource(R.drawable.shoe), null)if (hasDiscount) {Box(modifier = Modifier.align(Alignment.TopEnd).background(Color.Red, RoundedCornerShape(bottomStart = 6.dp))) {Text("-30%", color = Color.White)}}}

即使 if 条件不成立,Compose 还是会走一次测量逻辑,哪怕只是“跳过绘制”,我们依然付出了性能开销。在一个电商首页有几十几百个商品卡片时,这就是“白干活”。

优化思路

此时可以使用 SubcomposeLayout ,按需延迟渲染,真正做到“有才测,没有就不测”。优化后的代码如下所示:

@Composablefun ProductCard(image: Int, hasDiscount: Boolean) {SubcomposeLayout { constraints ->val main = subcompose("main") { Image(painterResource(image), null, Modifier.fillMaxWidth.height(150.dp)) }.map { it.measure(constraints) }val badge = if (hasDiscount) { subcompose("badge") { Box( Modifier.background(Color.Red, RoundedCornerShape(bottomStart = 6.dp)) .padding(4.dp) ) { Text("-30%", color = Color.White) } }.map { it.measure(constraints) } } else emptyListval w = main.maxOf { it.width } val h = main.maxOf { it.height } layout(w, h) { main.forEach { it.place(0, 0) } badge.forEach { it.place(w - it.width, 0) } } } }

真正做到“有才渲染”,没折扣就连角标的测量和内存分配都省掉了。在这种电商场景下,如果一屏 10 个商品,只有 2 个打折,那就是节省了 80% 的角标计算开销。

记得刚开始写 Compose 那会儿,最经典的“坑”是使用 Column + verticalScroll 显示长列表,没问题,能跑,但一旦数据量大呢(比如 500 条消息),内存直接飙升,掉帧明显。

Column(Modifier.verticalScroll(rememberScrollState)) {messages.forEach {MessageBubble(it.text, it.time)}}

verticalScroll 会把 所有子项一次性渲染出来 ,不管用户看不看得见。假设一条消息渲染需要 5ms,500 条就是 2.5 秒,必卡无疑。

优化思路

必须得用 LazyColumn / LazyRow 。它会只渲染屏幕可见范围内的子项,不可见的会被丢弃或复用。

LazyColumn {items(messages, key = { it.id }) { msg ->MessageBubble(msg.text, msg.time) } }

列表场景必须用 Lazy 系列,否则就是性能地雷。不夸张地说,一个 1000 条的聊天记录,用 Column 会直接卡死,用 LazyColumn 依然能丝滑滑动。如果是轮播、分页场景,还可以用 HorizontalPager/VerticalPager ,同样是 按需渲染 ,性能上可以保证。

还是那句话,工欲善其事必先利其器

Compose 的性能问题,不靠“肉眼猜测”,而要靠“火眼金睛”——也就是工具,只有定位到问题所在,才能快速解决优化它们。所以想要快速定位到性能问题,还是得合理利用监测工具。

这个工具是 AS 自带的,大家应该都不陌生了吧,上一篇我们进行重组次数的检测也是需要用到它的。通过 Layout Inspector 可以看到每个 Composable 背后的“骨架”:层级有多深、谁在频繁刷新。

怎么说呢,如果我们看到某个卡片 Card → Column → Row → 一大堆 Box → 里面还有嵌套的 LazyColumn ,层级深得像“俄罗斯套娃”,那就是优化信号⚠️。然后再点开 Recomposition Counts ,哪个节点变红、数值飙升,说明它在“疯狂重组”,当然我们这篇重点是关注层级嵌套,避免层级过多,陷入套娃风波。

使用工具分析指标层级 (Hierarchy Depth) : Layout Inspector 左侧的树结构,可以直观看到是否“太多父子嵌套”。测量耗时 (Layout/Measure) :在 System Tracing 里, measure 调用链条过长时,可能就是层级拖累。绘制耗时 (Draw) :越多嵌套容器, Draw 阶段也要“层层透传”。

类比一下,嵌套布局== 一边走路还要层层过安检,速度自然慢;扁平化布局==一条直路开车。我们优化的方向就是需要尽量在不影响功能的情况下,将过多嵌套的布局向扁平化发展。

下面我们来一次“带你走进案发现场”的排查实战。

之前有个 APP 首页做了图片轮播 Banner,每3秒自动滚动的需求;

有一天测试同学找到了我说:"小江啊,这个图片轮播的 Banner,手动滑动还挺顺的,但自动切换的时候,总感觉像放 PPT 一样有点卡顿,会掉帧,不够丝滑"。

我一听心里一咯噔,难道是图片渲染耗时导致了?记得之前不是有排查过这里么。我立马晃了晃脑袋,开始定位问题。

首先我打开了 Layout Inspector ,对着那块图片轮播部分代码一看,好家伙,UI 树长得像”俄罗斯套娃“,虽然说第一版没啥要求,但这也太随心所欲了吧,谁写的代码?一看提交记录,哦,我自己写的,那没事了

本来只是展示一张图片,结果套了 6、7 层壳,你不慢谁慢啊,我真佩服自己。

接着我打开了 System Tracing ,观察切换瞬间的 measure 和 draw :

measure 阶段耗时明显超标(大约24ms),超过了 16ms 的一帧时间预算。树越深,每次切图就得“从顶楼量到底楼”,一趟下来自然慢。

此时嫌疑人锁定:由于我们过渡嵌套导致的。

此时想了想,其实根本不需要那么多套娃,最核心的就是一个容器放图片,于是乎,开始优化了下,大致代码如下

@Composablefun BannerOptimized(images: List) {val screenWidth = LocalConfiguration.current.screenWidthDp.dpval listState = rememberLazyListStateLaunchedEffect(images.size) {var index = 0while (true) {delay(3000)index = (index + 1) % images.sizelistState.animateScrollToItem(index)}}LazyRow( state = listState, modifier = Modifier.fillMaxSize, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(images.size) { index ->Box( modifier = Modifier .width(screenWidth) .height(180.dp) .clip(RoundedCornerShape(8.dp)) .background(Color.White) .border(1.dp, Color.White, RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center ) { Image( painter = painterResource(images[index]), contentDescription = null, modifier = Modifier.fillMaxWidth, contentScale = ContentScale.Crop ) } } } }

此时再打开 Layout Inspector 检测下

层级瞬间从 7 层 → 3 层,测量和绘制链条直线缩短。

此时在把 App 跑起来测下

measure 耗时从 24ms 降到 12ms;切图动画流畅,不卡顿;

原来并不是 Compose 渲染太慢了,而是“嵌套套娃”拖垮了性能,这样我们只要结构扁平化,性能问题就迎刃而解。放一张对比图,大家可以更直观感受下优化后的结构。

结语

好了,上面听笔者唠叨了这么久,也该收收尾了。想要真正写出丝滑的 Compose 布局,还需要养成一些日常习惯,这里笔者总结了高性能布局的七个准则,当然这不是死规则,仅供参考,这更像是一种开发习惯,一旦习惯养成,我们会发现布局写得更轻盈、滑动更流畅,团队协作时也更容易维护。

嵌套层级是否控制在4层以内?超过5个项的列表是否使用 LazyColumn/LazyRow ?相邻的 Column/Row 是否合并?是否避免在布局阶段进行耗时计算?图片或者视频资源尺寸是否预先确定?是否用 IntrinsicSize 替代嵌套测量?是否定期用 Layout Inspector 检查布局耗时?

“记住:每一个多余的嵌套布局,都在透支你的性能预算!”

未完待续,下一篇预告《Compose 动画与过渡效果:从“卡顿掉帧”到“好莱坞级丝滑”》

动画卡顿的常见原因及识别方法如何处理中断/取消动画以防 UI 状态错乱多段动画如何优雅衔接?组合与协同技巧如何调试帧率与过渡流畅度?

要一直向前看,舍得不曾舍得的舍得会舍得,习惯不曾习惯的习惯会习惯。OK,各位同学,就到这里吧,我们下一篇不见不散!!

来源:墨码行者一点号

相关推荐