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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 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的踩坑之路任重而道远,许多奇奇怪怪的现象背后都是简单的原理,搞懂了原理一切疑惑都能得到解答,如本文中提到的关于遍历带来的奇怪现象,其实就是声明式布局对于列表处理的通病,常用的解决方式就是通过声明主键来让组件最大程度优化性能。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
算法 安全 数据安全/隐私保护
深入探究一个长期隐藏的底层bug的学习报告
在软件开发的过程中,底层bug往往像一颗定时炸弹,随时可能引发严重的问题。本文将分享我在开发过程中遇到的一个长期未被发现的底层bug,以及我如何逐步排查并最终解决这个问题的全过程。通过这次排查,我深刻认识到了代码规范性的重要性。一个不规范的代码修改,虽然短期内可能不会引起问题,但长期累积下来,可能会引发灾难性的后果。此外,我也意识到了底层模块的通用性和风险意识的重要性。在解决一个问题的同时,应该审视是否有相似的问题存在,以避免未来的风险。
96 3
|
3月前
|
前端开发
【针对游戏开发&CG制作的搬砖人员的资源搜索技巧分享—持续补充篇】
【针对游戏开发&CG制作的搬砖人员的资源搜索技巧分享—持续补充篇】
|
Java BI 数据库
特别诺贝尔奖论文《天赋与运气:随机性在成功与失败中的作用》代码实现简版(JAVA)
特别诺贝尔奖论文《天赋与运气:随机性在成功与失败中的作用》代码实现简版(JAVA)
|
12月前
|
测试技术
《游戏测试》经典BUG解析001--002
《游戏测试》经典BUG解析001--002
|
计算机视觉
队列的概念及结构(内有成型代码可供CV工程师参考)
队列的概念及结构(内有成型代码可供CV工程师参考)
63 0
算法入门小题目——点击消除
算法入门小题目——点击消除
|
算法 C++
【软/自考】算法实用技巧——递归VS迭代
【软/自考】算法实用技巧——递归VS迭代
71 0
|
存储 安全 算法
从“Back to Basic”到伙伴优先,阿里云的组合拳总算整明白了
阿里巴巴最近又活跃了起来——不是在天猫,也不是在支付宝,而是在技术端。 5月26日,阿里云发布了2022财年财报,营收首次超过千亿达到1001.8亿元,同时首次实现年度盈利(11.46亿元); 6月13日,阿里云智能总裁张建锋在2022年阿里云峰会上发布年度策略“Back to Basic”,发布了云数据中心专用处理器CIPU,提出要在技术长征路上不懈努力赢取新的突破;
315 0
|
机器学习/深度学习 算法
Bounding Box Regression超详解(全站最全汇总版)综合各个途径文档 看这一篇就够了 解决你所有疑惑
Bounding Box Regression超详解(全站最全汇总版)综合各个途径文档 看这一篇就够了 解决你所有疑惑
Bounding Box Regression超详解(全站最全汇总版)综合各个途径文档 看这一篇就够了 解决你所有疑惑