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日志并进行多维度分析。
相关文章
|
6月前
|
编译器 API 容器
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
214 0
|
Go
Go语言编程的一大杀器!详解defer语句
Go语言编程的一大杀器!详解defer语句
65 0
|
10天前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
19 1
|
6月前
|
算法 安全 数据安全/隐私保护
深入探究一个长期隐藏的底层bug的学习报告
在软件开发的过程中,底层bug往往像一颗定时炸弹,随时可能引发严重的问题。本文将分享我在开发过程中遇到的一个长期未被发现的底层bug,以及我如何逐步排查并最终解决这个问题的全过程。通过这次排查,我深刻认识到了代码规范性的重要性。一个不规范的代码修改,虽然短期内可能不会引起问题,但长期累积下来,可能会引发灾难性的后果。此外,我也意识到了底层模块的通用性和风险意识的重要性。在解决一个问题的同时,应该审视是否有相似的问题存在,以避免未来的风险。
120 3
|
6月前
|
存储 Python
[重学Python]Day3 函数和模块的使用
本文介绍了Python中的函数和模块的使用。函数用于避免代码重复,通过`def`定义,参数可有默认值或可变参数。模块管理同名函数,通过`import`导入。示例包括计算最大公约数和最小公倍数、判断回文数和素数的函数,以及检测回文素数的程序。
41 0
|
程序员 C语言 C++
轻轻松松几分钟,看完锤爆流程控制结构。
轻轻松松几分钟,看完锤爆流程控制结构。
|
算法 C++
【软/自考】算法实用技巧——递归VS迭代
【软/自考】算法实用技巧——递归VS迭代
87 0
|
前端开发 算法 API
《通过减少 draw call 提升渲染性能-沧东》演讲视频 + 文字版
《通过减少 draw call 提升渲染性能-沧东》演讲视频 + 文字版
243 0
|
数据安全/隐私保护 C++ Python
深度之眼(十七)——Python标准库(上)
深度之眼(十七)——Python标准库(上)
143 0
深度之眼(十七)——Python标准库(上)
|
Python
深度之眼(十七)——Python标准库(下)
深度之眼(十七)——Python标准库(下)
126 0
深度之眼(十七)——Python标准库(下)