Compose:警惕Loop(遍历),图文并茂带你深度释疑,解决的不仅是性能问题

简介: Compose:警惕Loop(遍历),图文并茂带你深度释疑,解决的不仅是性能问题

1.什么是Loop陷阱?


在Compose的开发过程中,经常遇到会使用到循环的地方,但是简单的循环背后竟然隐含着巨大的性能隐患甚至是奇怪的UI问题,下面看看这个案例:

image.png

UI非常简单,就是一个初始5行的列表,点击按钮后在头部插入一个元素,由于LaunchedEffect的特性,我们可以监听某个重组作用域是否生成了,如果你不懂LaunchedEffect,可以看看笔者的这篇文章:

好的回归正题,启动App之后,得到如下日志:

image.png

没什么问题,App刚进来的时候,Compose检测出列表有5项元素,于是生成了5个重组作用域,于是`LaunchedEffect`也有5个、日志打印了5次。

点击一下按钮,让列表的头部插入一个元素,那么会打印什么样的日志呢?请读者自己先思考5秒:

5秒...

4秒...

3秒...

2秒...

1秒...

揭露答案:

image.png

答案是最后一个元素重新被打印了,哎哟我去,Compose玩花的是吧,插入的是第0个,倒是最后一个给我打印了,发生什么事了

image.png

我埋个小伏笔,抛开这个问题不谈,我们使用布局查看器观察一下重组次数:

image.png

又发生什么事了,我们观察到,插入新元素之后,新增了一个组件,除去他是新增的没有计算到重组次数以外,其余的Text都重组了一次,发生什么事了?更让人疑惑的是,不是插入在开头的元素吗,为什么是最后一个是新增的?

先别急,下面正式进入解惑部分。


2.Loop陷阱的本质:萝卜和坑没绑定


程序员在Compose里面调用forEach的时候,实际上是做了两件事:1.生成N个坑位 2.把数据依次插入坑位。为了降低读者的理解难度,下面使用几张图来解释:

2.1.根据List的长度生成N个坑位

image.png什么是坑位?这是笔者为了方便读者理解生造的词,一个萝卜一个坑嘛,其实这里的坑位就是重组作用域,因为列表有5个元素,因此生成了5个重组作用域,这就是LaunchedEffect被调用了5次的原因

2.2.把数据依次插入坑位

这个没什么好说的,数据一一填入了自己的坑,一个萝卜一个坑。

image.png

2.3.插入新数据,坑爹的地方在此

回想一下我们刚才的步骤,Compose挖了N个坑,然后依次埋萝卜,这有什么问题?这会导致坑位和萝卜不是一一对应的,实际如下图:

image.png好家伙,假如你在地里种了5个萝卜,这时候要在田的开头多种一个萝卜,Compose做了以下的事:

把新的萝卜埋在第一个坑,其余的每一个萝卜都挖起来埋到下一个坑去,最后一个萝卜埋新挖的坑

因此,新挖了一个坑(导致LaunchEffect新调用了一次,而且打印的是最后一个数据),然后全部坑都种了新的萝卜(原来5个坑位发生了重组),这就是开头展示的两个怪现象的本质原因。

image.png

那么有没有一种办法,让每个萝卜在种下去之后就不要离开他自己的坑位了,新加入的萝卜才需要重新挖一个坑呢?有的,请往下看。


3.一个萝卜一个坑,新来萝卜别换坑


如何让每个坑位都能认出他的萝卜呢,答案是使用key这个api,简单把代码改造成如下:

image.png

这里使用key包裹住刚才的代码,key里面的参数使用每个Item的主键,这样每个坑位就和他的萝卜绑定了,不会再做那种把萝卜1搬到坑2的情况,而是坑1和萝卜1对应起来,为了验证正确,我们点一下按钮后,查看一下日志:

image.png

同样是输出了一个日志,但是这个日志对应的是新插入的内容,说明新增的坑位和新增的萝卜是一一对应的,这次不是在最后一个位置新增了坑位,而是在开头新增了坑位,让我们再看看重组的情况:

image.png

可以看到,原来的5个Text(即后面5个Text)虽然也进入了重组阶段,但是由于每一个坑位的萝卜没有发生变化,他们都因为智能重组的机制跳过了重组阶段,而第一个Text(即新插入的Text)由于是新建的,没有发生重组,一切都符合我们的预期了,如果你还不懂,看看下面这张图:

image.png

image.png

简单处理后就比较符合我们的自然逻辑了,Compose默认的行为真的有点反直觉,但是想想也有道理,声明式布局都是自动绑定UI和数据的,如果你不主动声明每一个数据的主键是什么,Compose又如何知道变化后的列表中,哪些数据是新增的,哪些数据是发生了变化的,哪些数据是删除掉了呢,他只能当做整个列表都是变化过的,一视同仁给你全部重组了,这有点类似RecyclerViewnotifyDatasetChanged,我们享受了声明式布局的组件与数据对象自动绑定的便利,同样要付出相应的心智成本。


4.多谈一点,但不多谈


在Comopse的一些懒加载的组件中,例如常见的LazyColumnLazyRowitems中,同样存在key这个属性,道理都是一样的,就是给告诉Compose如何识别一个数据源,让列表发生后,自动找出变化了的数据,而不是对列表进行整体的重组,提高效率,具体不展开说了,因为官方文档讲的很详细,需要的朋友可以自行阅读官方文档


总结

Compose的踩坑之路任重而道远,许多奇奇怪怪的现象背后都是简单的原理,搞懂了原理一切疑惑都能得到解答,如本文中提到的关于遍历带来的奇怪现象,其实就是声明式布局对于列表处理的通病,常用的解决方式就是通过声明主键来让组件最大程度优化性能。

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
|
Android开发
解决在Android Compose中点击空白处收回软键盘
解决在Android Compose中点击空白处收回软键盘
639 0
|
人工智能 自然语言处理 前端开发
从客服场景谈:大模型如何接入业务系统
本文探讨了大模型在AI客服中的应用。大模型虽具有强大的知识生成能力,但在处理具体业务如订单咨询、物流跟踪等问题时,需结合数据库查询、API调用等手段。文章提出用Function Call连接大模型与业务系统,允许大模型调用函数获取私域知识。通过具体示例展示了如何设计系统提示词、实现多轮对话、定义Function Call函数,并利用RAG技术检索文档内容。最后,展示了该方案在订单查询和产品咨询中的实际效果。
|
Android开发 Kotlin
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
3561 0
【错误记录】Kotlin 编译报错 ( Type mismatch: inferred type is String? but String was expected )
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation 'org.greenrobot:eventbus:3.3.1'`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
Java Maven Kotlin
在 build.gradle.kts 添加 阿里云仓库
在 build.gradle.kts 添加 阿里云仓库
3205 0
|
XML API Android开发
构建高效的安卓应用:使用Jetpack Compose实现动态UI
【4月更文挑战第13天】 在移动应用开发领域,随着用户对流畅体验和即时反馈的期待不断上升,开发者面临着构建高效、响应式且具有丰富交互性的用户界面的挑战。传统的Android开发方法,如基于XML的布局,虽然稳定但往往伴随着较高的资源消耗和较低的开发效率。本文将探讨如何使用Jetpack Compose——一种现代声明式UI工具包,来构建动态且高效的安卓应用界面。通过深入分析Jetpack Compose的核心原理及其与传统方法的对比,揭示如何利用其强大的功能集合提升应用性能和开发效率。我们将通过实例演示如何快速构建可重用组件、实现实时数据绑定,以及优化布局渲染过程,从而为开发者提供一种更简洁、
|
Android开发
Android 获取Wifi开关状态、控制Wifi开关
Android 获取Wifi开关状态、控制Wifi开关
692 0
|
Android开发 Kotlin 容器
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)
711 1
|
Java 关系型数据库 MySQL
springboot整合jpa自动创建mysql表以及遇到的一些问题记录
springboot整合jpa自动创建mysql表以及遇到的一些问题记录
675 0

热门文章

最新文章

下一篇
开通oss服务