01 前言 长列表是前端和客户端应用中最常见的业务场景,比如商品瀑布流等,有成千上万条数据,因此长列表的渲染性能在iOS,Android,Harmony,Web等各大平台都非常重要。HarmonyOS和iOS类似也提供了自己的解决方案。Roma(罗码,京东科技一码多端解决方案)作为跨端平台,在此基础上进行了具体的实践。在实践过程中,遇到了各种问题和挑战,经历了ArkTS+C++架构向纯C++架构的转变,本文将围绕实践中的各种问题和挑战,探讨Roma的具体解决方案和优化思路。
02 鸿蒙长列表解决方案及原理
长列表是前端和客户端应用中最常见的业务场景,比如商品瀑布流等,有成千上万条数据,因此长列表的渲染性能在iOS,Android,Harmony,Web等各大平台都非常重要。HarmonyOS和iOS类似也提供了自己的解决方案。Roma(罗码,京东科技一码多端解决方案)作为跨端平台,在此基础上进行了具体的实践。在实践过程中,遇到了各种问题和挑战,经历了ArkTS+C++架构向纯C++架构的转变,本文将围绕实践中的各种问题和挑战,探讨Roma的具体解决方案和优化思路。
鸿蒙长列表解决方案及原理
1. 一次性加载方案(ForEach)
ForEach:一次性加载全量数据并循环渲染。原理如下:
(图片来自鸿蒙官网)
缺点:
1) 因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面加载渲染时间过长
2. 按需加载方案(LazyForEach)
LazyForEach:实现延迟加载数据并按需渲染。原理如下:
1) 根据屏幕可视区能够容纳显示的组件数量按需加载数据。
2) 根据加载的数据量创建组件,挂载在组件树上,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
3) 当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用;当组件滑入可视区域时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
(图片来自鸿蒙官网)
2.1 缓存列表项CacheCount
原理如下:
(图片来自鸿蒙官网)
2.2 组件复用@Reusable
框架为我们提供了组件复用的能力,机制如下:
1)标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,复用缓存可以通过reuseId标记为不同的缓存池。
2)当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从相应的复用缓存池中查找可复用的组件节点。
3)找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。
动态化的长列表解决方案
1. 移植iOS、Android方案到鸿蒙
1.1 其他两端的方案原理
缓存池大小设置为最大N页,每个方向N/2页(这里的N和摩擦系数等因素有关,这里暂时不详细展开,后面有机会专门写文章分享)
当组件滑出缓存区域外时,操作虚拟DOM树删除列表项节点,同时通过bridge在原生端进行相应列表项组件的销毁以降低内存占用;当组件滑入缓存区域时,操作虚拟DOM树添加列表项节点,同时通过bridge在原生端进行相应列表项组件的添加,这里从虚拟DOM节点到原生端的组件,都需要从头完成组件创建、挂载组件树这一过程,直至渲染到屏幕上。
原生端列表的reuseId是一个不会重复的唯一值
1.2 移植后存在的问题
UI层级过多。在ArkUI框架实现下,自定义组件本身必须增加一个包裹的容器,比如一个类似RomaDiv这样的业务里最常使用的,数量最多的自定义容器组件,里面必须有个类似Stack/Flex这样的容器组件才合法,因此这个组件本身就已经是两层了,比其他系统就多了一层。另外有些容器组件还有系统本身生成的类似__common__ 这种层级,也会导致层级变多。层级过多,每次创建,渲染过程中的计算就更多,耗时自然就更长。
跨语言通信链路长。原生组件的UI是基于ArkUI实现的,运行在方舟虚拟机中。JS代码运行在系统的JSVM中,在C++端,两种语言通过系统提供的NAPI通信,其中涉及各种数据类型转换,成本自然比其他系统要高。尤其在UI层级多的情况下,成本就更高了。
系统二次布局的问题。动态化系统架构中有三个核心线程:UI主线程,JS线程和布局计算的线程。布局方案采用的是yoga布局,可以高效地进行组件的大小,位置的计算。但是系统在此布局之后还会重新进行布局一次,这个开销就完全没有必要,但是却增加了耗时,影响了性能。
2. ArkTS版本解决方案
2.1 ArkTS方案原理
原生端UI完全依赖系统提供的懒加载LazyForEach + 缓存列表项CacheCount + 组件复用@Reusable,其中复用的reuseId设置为具体缓存池的类别。
虚拟DOM节点的创建,复用,回收和销毁的时机完全与原生端UI相对应的时机同步。由于ArkUI是声明式语法,因此整个过程是先由原生端触发UI占位,然后在对应的生命周期上相应的操作VDOM,再通过JSI&NAPI与原生端通信,更新原生端组件。
2.2 重点优化点
不要更新(可见+cacheCount)范围内的组件的键值key,此范围外的部分改变键值key
手动调用列表组件的方法只更新(可见+cacheCount)范围内的组件和对应的VDOM节点
没有正确使用组件复用 - 使用了组件复用,实际上是无效的复用,reuseId设置一定要正确,且必须为字符串类型
复用类型 | 描述 | 复用思路 |
有限变化型
组合型
嵌套型
优先使用@Builder替代自定义组件@Component,减少嵌套层级
避免不必要的状态变量刷新,使用AttributeUpdater更新组件属性
避免对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新
避免使用函数/方法作为复用组件创建时的入参
避免在列表滑动过程中做大量计算或者耗时长的操作
可以结合列表预加载,布局优化等其他常规手段进一步优化体验
3. C-API版本解决方案
3.1 C-API方案原理
系统提供了一个ArkUI_NodeAdapter对象来管理容器的子组件,这个对象类似事件的机制,通过相关事件通知按需生成组件。
(图片来自鸿蒙官网)
在监听事件的回调中处理创建,回收,复用,删除等逻辑。
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层级,不跨语言通信能进一步提高性能,带来更好的体验。
动态化是一个涉及JavaScript、C++、iOS、Android、Java、Harmony、Vue、Node、Webpack、Shell等众多领域的综合解决方案,我们有各个领域优秀的小伙伴共同前行,大家如果想深入了解某个领域的具体实现或者提出宝贵意见,欢迎在评论中留言,或关注京东技术公众号后台回复【技术交流】加入技术交流群,随时交流~
给Java同仁单点的AI"开胃菜"--搭建一个自己的本地问答系统