0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看威廉希尔官方网站 视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

鸿蒙跨端实践-长列表解决方案和性能优化

京东云 来源:jf_75140285 作者:jf_75140285 2024-09-23 15:26 次阅读

这是我参加创作者计划的第一篇文章。

前言

长列表是前端和客户端应用中最常见的业务场景,比如商品瀑布流等,有成千上万条数据,因此长列表的渲染性能在iOSAndroidHarmony,Web等各大平台都非常重要。HarmonyOS和iOS类似也提供了自己的解决方案。Roma(罗码)作为跨端平台,在此基础上进行了具体的实践。在实践过程中,遇到了各种问题和挑战,经历了ArkTS+C++架构向纯C++架构的转变,本文将围绕实践中的各种问题和挑战,探讨Roma的具体解决方案和优化思路。

一、鸿蒙长列表解决方案及原理

鸿蒙系统为List,WaterFlow,Grid等容器组件的数据加载和渲染提供了一次性加载方案(ForEach)和按需加载方案(LazyForEach)两种方式。

1. 一次性加载方案(ForEach)

•ForEach:一次性加载全量数据并循环渲染。原理如下:

wKgZombxGB6ABSp1AAFjOcKituY611.png

(图片来自鸿蒙官网)

缺点:

1) 因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面加载渲染时间过长

2) 屏幕可视区外的组件虽然不会显示在屏幕上,但是仍然会占用内存。在系统处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。

实际业务中数据条数非常多,该方案存在很严重的性能问题。为了解决这个性能问题,HarmonyOS提供了性能更好的解决方案

2. 按需加载方案(LazyForEach)

•LazyForEach: 实现延迟加载数据并按需渲染。原理如下:

1) 根据屏幕可视区能够容纳显示的组件数量按需加载数据。

2) 根据加载的数据量创建组件,挂载在组件树上,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。

3) 当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用;当组件滑入可视区域时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。

wKgaombxGCGAH0QmAAFAgYb_Lqw228.png

(图片来自鸿蒙官网)

LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。可以显著提升页面的能效比和用户体验。提升性能,HarmonyOS又给出了两种优化手段:缓存列表项(CacheCount)+组件复用(@Reusable)。

2.1 缓存列表项CacheCount

如果只有懒加载,滑动速度过快时,则会导致数据来不及加载而出现“白块现象”。为了解决这一问题,LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量。在设置cachedCount后,除屏幕内显示的ListItem组件外,还会预先将屏幕可视区外指定数量的列表项数据缓存起来。这样当缓存列表项需要从屏幕可视区外进入可视区内时,只用创建、渲染组件即可,相比不设置cachedCount提升了显示效率。(cacheCount具体设置多少,这里依然不详细展开,详见后续文章。)

原理如下:

wKgZombxGCKAQ270AAE0xU-FYYU215.png

(图片来自鸿蒙官网)

2.2 组件复用@Reusable

由上文可知LazyForEach+cacheCount方案中,当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用;当组件滑入可视区域时,需要从头完成组件创建、挂载组件树这一过程,直至渲染到屏幕上。而且列表页面很多列表项的UI样式完全相同,只有数据上的差异,如果能组件复用,就能节省组件创建的时间,因此就可以进一步提高列表页面的加载速度和响应速度。

框架为我们提供了组件复用的能力,机制如下:

1)标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,复用缓存可以通过reuseId标记为不同的缓存池。

2)当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从相应的复用缓存池中查找可复用的组件节点。

3)找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。

wKgaombxGCOAUUnKAAFe0mqXki4701.png

(图片来自鸿蒙官网)


二、动态化的长列表解决方案

结合上文HarmonyOS提供的解决方案,开始考虑动态化的长列表方案。通过前面鸿蒙跨端方案介绍文章,我们知道,跨平台框架的核心原理是通过JavaScript在JS引擎上执行时,对虚拟DOM进行操作,通过桥接或JSI与原生端进行通信,同时通过组件抽象,这些组件在不同平台上映射到相应的原生组件。运行时我们会有相应的节点树:JS虚拟DOM节点树->原生端组件节点树->原生端渲染节点树。长列表的渲染同样会涉及这三棵树,并且过程比较复杂。

1. 移植iOS、Android方案到鸿蒙

1.1 其他两端的方案原理

•缓存池大小设置为最大N页,每个方向N/2页(这里的N和摩擦系数等因素有关,这里暂时不详细展开,后面有机会专门写文章分享)

•当组件滑出缓存区域外时,操作虚拟DOM树删除列表项节点,同时通过bridge在原生端进行相应列表项组件的销毁以降低内存占用;当组件滑入缓存区域时,操作虚拟DOM树添加列表项节点,同时通过bridge在原生端进行相应列表项组件的添加,这里从虚拟DOM节点到原生端的组件,都需要从头完成组件创建、挂载组件树这一过程,直至渲染到屏幕上。

•原生端列表的reuseId是一个不会重复的唯一值

wKgZombxGCiAe0D7AA_8pu0GXX8174.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与原生端通信,更新原生端组件。


wKgaombxGCqAbym-AAmey3Jvn3U853.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自定义复用组件池,在整个应用中自由流转
嵌套型 复用组件的子组件的子组件存在差异 采用化归思想将嵌套问题转化为上面四种标准类型来解决
无法复用型 组件之间差别很大,规律性不强,子组件也不相同 不建议使用组件复用

wKgaombxGC2AdNdAAAGiKs3Fud8318.png

标准型

wKgZombxGC6ATCBBAAGGt9OFEqs739.png

有限变化型

wKgaombxGC-Af3S7AANF-M99znw689.png

组合型

wKgZombxGDGAb9g5AANoaNcqR7Q094.png

全局型

wKgZombxGDKAe2RvAALAO1NMTPY928.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对象来管理容器的子组件,这个对象类似事件的机制,通过相关事件通知按需生成组件。

wKgZombxGDOAcXEdAAH4PwqnZ3A821.png

(图片来自鸿蒙官网)


•在监听事件的回调中处理创建,回收,复用,删除等逻辑。

wKgaombxGDaAZ7ttAA66TlWHy7E258.png


3.2 部分核心代码

有兴趣的同学可以私下联系我。

4. 性能对比分析

使用JR APP购物车页面(页面结构较复杂),400条数据,分别用三种方案以及优化后测试,测试结果如下:

方案 ArkTS Create ArkTS Reuse C++ Reuse
完全显示所用时间 1s 804ms 1s 321ms 977ms
丢帧率 12.1% 0.0% 0.0%
独占内存 45.1M 42.3M 40.2M

测试结果表明,lazyForEach,组件复用,cacheCount,预加载等等这些方法的确提高了性能,尤其是滑动过程中出现的明显卡顿现象,同时减少UI层级,不跨语言通信能进一步提高性能,带来更好的体验。


三、总结

本文通过图文的方式介绍了HarmonyOS的长列表ArkTS解决方案以及原理,同时结合实际的实现过程介绍了ROMA动态化长列表的ArkTS和C++解决方案,相应的重点优化细节以及部分核心源码,最后对两者进行了性能对比分析。

如果大家觉得有帮助,千万别忘了点赞+收藏,方便以后随时阅读!

动态化是一个涉及JavaScript、C++、iOS、Android、Java、Harmony、Vue、Node、Webpack、Shell等众多领域的综合解决方案,我们有各个领域优秀的小伙伴共同前行,大家如果想深入了解某个领域的具体实现或者提出宝贵意见,可以在评论中给我留言,随时交流~!

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JS
    JS
    +关注

    关注

    0

    文章

    78

    浏览量

    18103
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2634

    浏览量

    66336
  • 鸿蒙
    +关注

    关注

    57

    文章

    2350

    浏览量

    42842
  • HarmonyOS
    +关注

    关注

    79

    文章

    1974

    浏览量

    30173
收藏 人收藏

    评论

    相关推荐

    鸿蒙原生开源库ViewPool在OpenHarmony社区正式上线

    方面的实践经验。它为鸿蒙生态的开发者和应用厂商提供了一套灵活高效的组件管理方案,有助于显著提升开发效率和应用
    的头像 发表于 12-20 14:44 201次阅读

    ShiMeta鸿蒙门禁考勤解决方案

    方案介绍ShiMeta鸿蒙门禁考勤解决方案由ShiMeta智慧通行管理系统、鸿蒙人脸识别门禁设备等软硬件组成,可提供稳定可靠的门禁、考勤和访客预约功能。
    的头像 发表于 12-13 16:45 119次阅读
    ShiMeta<b class='flag-5'>鸿蒙</b>门禁考勤<b class='flag-5'>解决方案</b>

    ShiMeta鸿蒙多屏同步拼接解决方案

    ►►►方案概述鸿蒙多屏同步解决方案在ShiMeta鸿蒙数字标牌系统的基础上,通过信号处理器,实现多个显示屏幕之间的内容同步显示,以提升信息传达的效率和视觉体验。
    的头像 发表于 12-13 16:45 120次阅读
    ShiMeta<b class='flag-5'>鸿蒙</b>多屏同步拼接<b class='flag-5'>解决方案</b>

    软通动力荣获华为鸿蒙生态“行业解决方案创新奖”

    华为开发者大会2024中,华为以“携手鸿蒙生态开发服务商,推动千行百业数智变革”为主题举办开发服务商专题论坛,展示鸿蒙生态最新进展及服务商合作模式,邀请优秀伙伴分享实践经验,并对在行业解决方案
    的头像 发表于 10-10 10:47 755次阅读

    揭秘动态化框架在鸿蒙系统下的高性能解决方案

    平台解决方案。 在研发团队使用后可大幅降低研发人力成本;为业务提供实时触达、A/B触达等能力以提升业务投放效率;同时保障了C用户优秀的用户体验。 一、动态化框架原理介绍  
    的头像 发表于 10-08 13:46 811次阅读
    揭秘动态化<b class='flag-5'>跨</b><b class='flag-5'>端</b>框架在<b class='flag-5'>鸿蒙</b>系统下的高<b class='flag-5'>性能解决方案</b>

    鸿蒙实践-JS虚拟机架构实现

    在Roma方案中,JS虚拟机是框架的核心,负责执行动态化的JS代码。在Android平台采用了基于V8的J2V8,iOS平台则使用了系统自带的JSCore,而在HarmonyOS中,由于业界无
    的头像 发表于 09-30 14:42 2411次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>跨</b><b class='flag-5'>端</b><b class='flag-5'>实践</b>-JS虚拟机架构实现

    基于TPS61094的长寿命、低成本智能电表解决方案

    电子发烧友网站提供《基于TPS61094的长寿命、低成本智能电表解决方案.pdf》资料免费下载
    发表于 09-24 10:47 9次下载
    基于TPS61094的长寿命、低成本智能电<b class='flag-5'>表解决方案</b>

    鸿蒙实践-布局方案介绍

    封装到标签中实现,业务只需要针对标签简单地设置相关属性,即可实现列表类布局,大幅提升研发效率。同时动态化也支持绝对布局以及控制视图的显示和隐藏等功能,使之能胜任绝大多数业务布局场景。 在京东金融App使用动态化方案适配鸿蒙系统的
    的头像 发表于 09-18 10:26 905次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>跨</b><b class='flag-5'>端</b><b class='flag-5'>实践</b>-布局<b class='flag-5'>方案</b>介绍

    MSPM0 L1测量仪表解决方案指南

    电子发烧友网站提供《MSPM0 L1测量仪表解决方案指南.pdf》资料免费下载
    发表于 09-04 10:47 1次下载
    MSPM0 L1测量仪<b class='flag-5'>表解决方案</b>指南

    恩智浦完整的Matter解决方案

    恩智浦为打造Matter设备,提供了完整的解决方案,从连接和安全解决方案到处理器和软件,应有尽有,为Matter标准的规模化商用提供有力支撑。
    的头像 发表于 08-26 18:04 2568次阅读
    恩智浦完整的Matter<b class='flag-5'>端</b>到<b class='flag-5'>端</b><b class='flag-5'>解决方案</b>

    广和通侧AI解决方案驱动性能密集型场景商用型场景商用

    2024世界机器人大会期间,广和通宣布:基于高通QCS8550平台的广和通侧AI解决方案高效使能性能密集型场景。该侧AI解决方案整合强大
    的头像 发表于 08-23 16:05 667次阅读
    广和通<b class='flag-5'>端</b>侧AI<b class='flag-5'>解决方案</b>驱动<b class='flag-5'>性能</b>密集型场景商用型场景商用

    基于GD32L233的物联网水表解决方案

    基于GD32L233的物联网水表解决方案采用了目前业界先进的窄带蜂窝通信威廉希尔官方网站 ,具有网络深覆盖、广链接、低功耗等优势,通信稳定、可靠、安全;采用工业级 NB-IoT模块和工业级 M2M 物联网卡,拥有攻击报警、电池低电报警,余额不足报警,欠费报警;可以实时显示水表用量、信号强度等数据信息。
    的头像 发表于 08-22 09:35 2020次阅读
    基于GD32L233的物联网水<b class='flag-5'>表解决方案</b>

    鸿蒙开发:应用组件设备交互(流转)【迁移】

    迁移的核心任务是将应用的当前状态(包括页面控件、状态变量等)无缝迁移到另一设备,从而在新设备上无缝接续应用体验。这意味着用户在一台设备上进行的操作可以在另一台设备的相同应用中快速切换并无缝衔接。
    的头像 发表于 06-11 17:10 1255次阅读
    <b class='flag-5'>鸿蒙</b>开发:应用组件<b class='flag-5'>跨</b>设备交互(流转)【<b class='flag-5'>跨</b><b class='flag-5'>端</b>迁移】

    鸿蒙ArkUI开发:常用布局【 创建列表(List)】

    列表容器是为了高效处理长列表的容器,能支持横向、竖向滚动,数据分组,分组头悬浮等功能
    的头像 发表于 05-15 15:30 787次阅读
    <b class='flag-5'>鸿蒙</b>ArkUI开发:常用布局【 创建<b class='flag-5'>列表</b>(List)】

    时钟域的解决方案

    在很久之前便陆续谈过亚稳态,FIFO,复位的设计。本次亦安做一个简单的总结,从宏观上给大家展示时钟域的解决方案
    的头像 发表于 01-08 09:42 905次阅读
    <b class='flag-5'>跨</b>时钟域的<b class='flag-5'>解决方案</b>