鸿蒙跨端实践-长列表解决方案和性能优化
作者:京东科技 徐超这是我参加创作者计划的第一篇文章。
前言
长列表是前端和客户端应用中最常见的业务场景,好比商品瀑布流等,有成千上万条数据,因此长列表的渲染性能在iOS,Android,Harmony,Web等各大平台都非常重要。HarmonyOS和iOS类似也提供了自己的解决方案。Roma(罗码)作为跨端平台,在此基础上进行了具体的实践。在实践过程中,碰到了各种问题和挑战,经历了ArkTS+C++架构向纯C++架构的转变,本文将围绕实践中的各种问题和挑战,探究Roma的具体解决方案和优化思绪。
一、鸿蒙长列表解决方案及原理
鸿蒙体系为List,WaterFlow,Grid等容器组件的数据加载和渲染提供了一次性加载方案(ForEach)和按需加载方案(LazyForEach)两种方式。
1. 一次性加载方案(ForEach)
•ForEach:一次性加载全量数据并循环渲染。原理如下:
https://img-blog.csdnimg.cn/img_convert/7c55dbd14c6498422947cfa9b1ec532d.png
(图片来自鸿蒙官网)
缺点:
1) 由于要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面加载渲染时间过长
2) 屏幕可视区外的组件固然不会表现在屏幕上,但是仍然会占用内存。在体系处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。
实际业务中数据条数非常多,该方案存在很严重的性能问题。为相识决这个性能问题,HarmonyOS提供了性能更好的解决方案
2. 按需加载方案(LazyForEach)
•LazyForEach: 实现耽误加载数据并按需渲染。原理如下:
1) 根据屏幕可视区可以大概容纳表现的组件数量按需加载数据。
2) 根据加载的数据量创建组件,挂载在组件树上,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
3) 当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用;当组件滑入可视区域时,需要重新完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
https://img-blog.csdnimg.cn/img_convert/4f159235e0ec628ee6a127a5456731c0.png
(图片来自鸿蒙官网)
LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,淘汰了页面首次启动时一次性加载数据的时间消耗,淘汰了内存峰值。可以显著提升页面的能效比和用户体验。提升性能,HarmonyOS又给出了两种优化本领: 缓存列表项(CacheCount) + 组件复用(@Reusable)。
2.1 缓存列表项CacheCount
假如只有懒加载,滑动速度过快时,则会导致数据来不及加载而出现“白块现象”。为相识决这一问题,LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量。在设置cachedCount后,除屏幕内表现的ListItem组件外,还会预先将屏幕可视区外指定命量的列表项数据缓存起来。这样当缓存列表项需要从屏幕可视区外进入可视区内时,只用创建、渲染组件即可,相比不设置cachedCount提升了表现服从。(cacheCount具体设置多少,这里依然不详细睁开,详见后续文章。)
原理如下:
https://img-blog.csdnimg.cn/img_convert/d6ccf49e69a4bbcea93b88f3e7361c55.png
(图片来自鸿蒙官网)
2.2 组件复用@Reusable
由上文可知LazyForEach+cacheCount方案中,当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用;当组件滑入可视区域时,需要重新完成组件创建、挂载组件树这一过程,直至渲染到屏幕上。而且列表页面很多列表项的UI样式完全相同,只有数据上的差别,假如能组件复用,就能节流组件创建的时间,因此就可以进一步提高列表页面的加载速度和响应速度。
框架为我们提供了组件复用的能力,机制如下:
1)标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,复用缓存可以通过reuseId标记为不同的缓存池。
2)当列表滑动新的ListItem将要被表现,List组件树上需要新建节点时,将会从相应的复用缓存池中查找可复用的组件节点。
3)找到可复用节点并对其进行更新后添加到组件树中。从而节流了组件节点和JSView对象的创建时间。
https://img-blog.csdnimg.cn/img_convert/a7bb1e9cf78a9af4e19ccc440c53022c.png
(图片来自鸿蒙官网)
二、动态化的长列表解决方案
联合上文HarmonyOS提供的解决方案,开始思量动态化的长列表方案。通过前面鸿蒙跨端方案先容文章,我们知道,跨平台框架的焦点原理是通过JavaScript在JS引擎上执行时,对虚拟DOM进行利用,通过桥接或JSI与原生端进行通讯,同时通过组件抽象,这些组件在不同平台上映射到相应的原生组件。运行时我们会有相应的节点树:JS虚拟DOM节点树 -> 原生端组件节点树 -> 原生端渲染节点树。长列表的渲染同样会涉及这三棵树,而且过程比力复杂。
1. 移植iOS、Android方案到鸿蒙
1.1 其他两端的方案原理
•缓存池大小设置为最大N页,每个方向N/2页(这里的N和摩擦系数等因素有关,这里临时不详细睁开,后面有时机专门写文章分享)
•当组件滑出缓存区域外时,利用虚拟DOM树删除列表项节点,同时通过bridge在原生端进行相应列表项组件的销毁以降低内存占用;当组件滑入缓存区域时,利用虚拟DOM树添加列表项节点,同时通过bridge在原生端进行相应列表项组件的添加,这里从虚拟DOM节点到原生端的组件,都需要重新完成组件创建、挂载组件树这一过程,直至渲染到屏幕上。
•原生端列表的reuseId是一个不会重复的唯一值
https://img-blog.csdnimg.cn/img_convert/f18a932cbb1b233a86b3afa62ddf6897.png
该方案已经被京东金融业务100+页面使用,在复杂的列表页面性能体现也非常好。优点也是显而易见,由于跨端的焦点原理决定了我们必须利用VDOM节点树和组件树,过程中涉及JS线程和UI线程的频仍通讯,终极举动是否一致,是否能达到我们想要的结果,这个过程涉及的细节非常多,因此一个简单的逻辑是包管正确性的比力好的本领。这固然也得益于iOS和Android体系本身性能的优越。从上文可知我们实在无论在VDOM节点树中,照旧原生端组件树中,新的VDOM节点/列表项组件创建或删除的时间,都没有复用节点或者利用体系本身的组件复用的能力,只有新创建和真删除,这种逻辑就非常简单明白,不容易产生bug。但是重新创建的过程会依赖体系本身的性能。
1.2 移植后存在的问题
然而,当我们把同样的方案移植到HarmonyOS上之后,使用ArkUI框架开发,发现肉眼可见的卡顿,抖动等掉帧现象非常严重,因此我们开始排查原因。并与iOS和Android体系进行对比分析,经过分析我们发现主要存在以下3个问题:
•UI层级过多。在ArkUI框架实现下,自定义组件本身必须增长一个包裹的容器,好比一个类似RomaDiv这样的业务里最常使用的,数量最多的自定义容器组件,内里必须有个类似Stack/Flex这样的容器组件才合法,因此这个组件本身就已经是两层了,比其他体系就多了一层。另外有些容器组件还有体系本身天生的类似__common__ 这种层级,也会导致层级变多。层级过多,每次创建,渲染过程中的计算就更多,耗时自然就更长。
•跨语言通讯链路长。原生组件的UI是基于ArkUI实现的,运行在方舟虚拟机中。JS代码运行在体系的JSVM中,在C++端,两种语言通过体系提供的NAPI通讯,其中涉及各种数据类型转换,本钱自然比其他体系要高。尤其在UI层级多的情况下,本钱就更高了。
•体系二次布局的问题。动态化体系架构中有三个焦点线程:UI主线程,JS线程和布局计算的线程。布局方案采用的是yoga布局,可以高效地进行组件的大小,位置的计算。但是体系在此布局之后还会重新进行布局一次,这个开销就完全没有须要,但是却增长了耗时,影响了性能。
针对这几个问题,经过和华为专家沟通以后,建议我们直接使用C-API开发,但是经过深入开发和沟通之后,发现C-API如今尚有功能欠缺,而且文档不完善,不能满意我们当下的所有需求,因此我们决定支持ArkTS版本和C-API版本两个版本,Q3先上线ArkTS版本,同时开发完CAPI版本,待华为进一步完善C-API后,Q4上线。
2. ArkTS版本解决方案
在已经存在以上问题的前提下,我们需要尽可能的提高列表性能,创建慢的问题,起首思量到的就是reuse的思绪。
2.1 ArkTS方案原理
•原生端UI完全依赖体系提供的懒加载LazyForEach + 缓存列表项CacheCount + 组件复用@Reusable,其中复用的reuseId设置为具体缓存池的类别。
•虚拟DOM节点的创建,复用,接纳和销毁的时机完全与原生端UI相对应的时机同步。由于ArkUI是声明式语法,因此整个过程是先由原生端触发UI占位,然后在对应的生命周期上相应的利用VDOM,再通过JSI&NAPI与原生端通讯,更新原生端组件。
https://img-blog.csdnimg.cn/img_convert/19135df06bfaddaa756d56b29834c8a3.png
这个方案是真正做到了reuse/recycle的长列表,做到了比力丝滑的体验。但是由于有了recycle/reuse的过程,也增长了更多的复杂性,有很多细节需要处置惩罚。
2.2 重点优化点
1)更新数据后UI“闪”的问题 - 不要改变键值key + @ObjectLink + @Observed
这个问题的根本原因是lazyForEach的迭代器key generator的键值key发生了变革。假如键值key发生了变革,框架会将这个变革的组件整体先接纳,然后再重新创建。经历这一个过程就会出现“闪”的问题。
而且,改变键值key去刷新UI的方式代价很大,同一类别的列表项的布局非常类似,只是表现的文本和图片等不一样,稳定化的组件不需要重新创建,只需要更新变革的部分即可。这种情况框架提供了装饰器@Observed和@ObjectLink,可以监听变革的部进行局部更新。同时,复杂列心情况下,数据源大多都是多层嵌套的对象布局,建议使用@ObjectLink而不要用@Prop,由于@Prop会进行深拷贝,会增长创建时间及内存的消耗,开销较大,而@ObjectLink指向数据源的指针,双向同步数据,因此这种情况下性能更优。
2)刷新/更新数据后,数据先展示其他的数据然后快速再刷成终极结果
• 不要更新(可见+cacheCount)范围内的组件的键值key,此范围外的部分改变键值key
•手动调用列表组件的方法只更新(可见+cacheCount)范围内的组件和对应的VDOM节点
起首产生这个问题的原因照旧由于key发生了变革,每次重新创建的时间,假如当前类型的缓存池有数据,就从缓存池取出复用,然后再更新变革的部分。这个从缓存池取出的组件仍然带有原来的数据信息,因此我们会看到先展示其他数据然后再被刷成终极结果。为了避免这个现象,起首照旧不要改变key。在UI上就是已经渲染了的那些组件,也即可视加上cacheCount范围内的组件。同时对此范围内的组件手动调用组件的更新方法,更新组件,这时JS引擎会对这个节点进行diff,把变革的部分通过JSI与原生端通讯,原生端完成终极UI的更新。范围外的部分就按需更新key和数据源。
3)有些列表滑动过程中仍有卡顿现象
• 没有正确使用组件复用 - 使用了组件复用,实际上是无效的复用,reuseId设置肯定要正确,且必须为字符串类型
复用类型形貌复用思绪标准型复用组件之间布局完全相同标准复用有限变革型复用组件之间有不同,但是类型有限使用reuseId或者独立成两个自定义组件组合型复用组件之间有不同,情况非常多,但是拥有共同的子组件将复用组件改为Builder,让内部子组件相互之间复用全局型组件可在不同的父组件中复用,而且不得当使用@Builder使用BuilderNode自定义复用组件池,在整个应用中自由流转嵌套型复用组件的子组件的子组件存在差别采用化归头脑将嵌套问题转化为上面四种标准类型来解决无法复用型组件之间差别很大,规律性不强,子组件也不相同不建议使用组件复用
https://img-blog.csdnimg.cn/img_convert/4c811d3fbc53e0eb7883c436c853b988.png
标准型
https://img-blog.csdnimg.cn/img_convert/b19c0c3075c69e522b15ea67c24c8237.png
有限变革型
https://img-blog.csdnimg.cn/img_convert/5ea463251c3dfa74c0880e82eb06cdd5.png
组合型
https://img-blog.csdnimg.cn/img_convert/4b36ccdac7c3eb6627cc16e1a4c25a95.png
全局型
https://img-blog.csdnimg.cn/img_convert/7f82cc65e0855746d95ed59e0cdcf5d5.png
嵌套型
此外,假如使用if/else条件语句来控制布局的布局,会导致在不同逻辑创建不同布局布局嵌套的组件,此时我们应该使用reuseId将if/else条件语句拆分为不同布局的组件
•优先使用@Builder替代自定义组件@Component,淘汰嵌套层级
ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和和RenderNod渲染的耗时,因此应该优先使用@Builder。同时淘汰一个自定义组件,也就是淘汰一次aboutToReuse的回调,也会节流耗时。
•避免不须要的状态变量刷新,使用AttributeUpdater更新组件属性
•避免对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新
•避免使用函数/方法作为复用组件创建时的入参
•避免在列表滑动过程中做大量计算或者耗时长的利用
•可以联合列表预加载,布局优化等其他常规本领进一步优化体验
3. C-API版本解决方案
上文中我们已经提到CAPI的方案能解决UI层级过多,跨语言通讯链路长两个焦点问题,同时也淘汰了状态变量维护相应的耗时,是我们终极的解决方案。C++端我们照旧采用了recycle/reuse的方案,C-API实现上我们需要自己实现类似lazyForEach的能力。
3.1 C-API方案原理
• 体系提供了一个ArkUI_NodeAdapter对象来管理容器的子组件,这个对象类似变乱的机制,通过相关变乱通知按需天生组件。
https://img-blog.csdnimg.cn/img_convert/957aa31cf085ec707efabf104531ad3d.png
(图片来自鸿蒙官网)
•在监听变乱的回调中处置惩罚创建,接纳,复用,删除等逻辑。
https://img-blog.csdnimg.cn/img_convert/ad558fada0f37d59776a0de8907130a2.png
3.2 部分焦点代码
有兴趣的同学可以私下接洽我。
4. 性能对比分析
使用JR APP购物车页面(页面布局较复杂),400条数据,分别用三种方案以及优化后测试,测试结果如下:
方案ArkTS CreateArkTS ReuseC++ Reuse完全表现所用时间1s 804ms1s 321ms977ms丢帧率12.1%0.0%0.0%独占内存45.1M42.3M40.2M 测试结果表明,lazyForEach,组件复用,cacheCount,预加载等等这些方法的确提高了性能,尤其是滑动过程中出现的明显卡顿现象,同时淘汰UI层级,不跨语言通讯能进一步提高性能,带来更好的体验。
三、总结
本文通过图文的方式先容了HarmonyOS的长列表ArkTS解决方案以及原理,同时联合实际的实现过程先容了ROMA动态化长列表的ArkTS和C++解决方案,相应的重点优化细节以及部分焦点源码,末了对两者进行了性能对比分析。
假如大家觉得有资助,千万别忘了点赞+收藏,方便以后随时阅读!
动态化是一个涉及JavaScript、C++、iOS、Android、Java、Harmony、Vue、Node、Webpack、Shell等浩繁领域的综合解决方案,我们有各个领域优秀的小搭档共同前行,大家假如想深入相识某个领域的具体实现或者提出宝贵意见,可以在评论中给我留言,随时交流~!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]