往期推文看点
- 鸿蒙(HarmonyOS)北向开发知识点记录~
- 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~
- 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
- 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
- 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
- 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
- 记录一场鸿蒙开发岗位面试经历~
- 持续更新中……
概述
列表是应用开发中最常见的一类开发场景,它可以将杂乱的信息整理成有规律、易于理解和操作的形式,便于用户查找和获取所需要的信息。应用程序中常见的列表场景有新闻列表、购物车列表、各类排行榜等。随着信息数据的累积,特别是一些新闻应用、购物应用、聊天应用,列表数据往往会达到上万条,针对这类大量数据加载的长列表应用,如何对长列表的性能进行优化是非常重要的。一个正确、高性能的长列表应用能明显降低列表渲染时间、提升页面的滑动帧率、降低应用内存占用,大幅提升用户体验。
针对长列表加载这一场景,本文将介绍如下5种优化手段,通过这些优化手段的单个使用或组合使用,可以对列表渲染时间、页面滑动帧率、应用内存占用等方面带来优化,提升性能和用户体验:
- 懒加载:提供列表数据按需加载能力,解决一次性加载长列表数据耗时长、占用过多资源的问题,可以提升页面响应速度。
- 缓存列表项:提供屏幕可视区域外列表项长度的自定义调节能力,配合懒加载设置可缓存列表项参数,通过预加载数据提升列表滑动体验。
- 动态预加载:根据历史任务加载耗时情况,动态调整屏幕可视区域外数据预取数量,配合懒加载设置,可在列表不断滑动时,屏幕可视区外实时更新列表数据,通过预取和预渲染数据提升列表滑动体验**。**
- 组件复用:提供可复用组件对象的缓存资源池,通过重复使用已经创建过并缓存的组件对象,降低相同组件短时间内频繁创建和销毁的开销,提升组件渲染效率。
- 布局优化:使用扁平化布局方案,减少视图嵌套层级和组件数,避免过度绘制,提升页面渲染效率。
下文将以 “HMOS世界” 中首屏的长列表加载为例,通过5个测试来验证列表优化前后性能的收益,以证明这些优化手段的可行性。综合考虑业界共识指标和实际用户使用体验,测试将对比分析如下几个关键指标:
- 完全显示所用时间(Time To Full Display, TTFD):表示应用生成具有完整内容的第一帧所用时间,包括在第一帧之后异步加载的内容。本文测量的是不同数据量下长列表首次加载到屏幕上所用的时间。
- 丢帧率(Janky Frames):表示一个时间周期内的丢帧比率,指一个时间周期内有问题的帧比例。HarmonyOS系统要求每一帧都要在 11.1ms(90Hz刷新率)内绘制完成,如果页面没有在 11.1ms内完成这一帧的绘制,就会出现丢帧。部分丢帧一般用户肉眼是感知不到的,只有出现连续丢帧用户才有明显感知。
- 独占内存(Unique Set Size,USS):一个进程所占用的私有内存,即该进程独占的内存。 它反映了运行一个特定进程真实的边际成本(增量成本)。当一个进程被销毁后,独占内存是真实返回给系统的内存。当进程中存在可疑的内存泄露时,独占内存是最佳观察数据。
测试表明,使用LazyForEach懒加载这项技术后,相比ForEach这种加载方式,在列表数据量较小(100条内)且数据一次性全量加载不是性能瓶颈时,两者各项性能指标差异不大。但当列表数据较长特别是达到10000条数据量后,ForEach的各项性能指标会有“指数级别”的显著劣化,滑动会出现明显的卡顿,甚至会出现应用crash等现象;而LazyForEach因为采用了懒加载、缓存列表项、组件复用等技术后,能明显减少首屏完全显示所用时间,降低应用的独占内存,提高页面滑动帧率,带来更好的性能。其对比效果如下所示:
**图1 **10000条数据量下ForEach和LazyForEach最佳实践启动对比
图210000条数据量下ForEach和LazyForEach最佳实践滑动对比
懒加载
原理介绍
HarmonyOS应用框架为容器类组件的数据加载和渲染提供了2种方式:
- 方式一,循环渲染
通过 循环渲染(ForEach) 从数组中获取数据,并为每个数据项创建相应的组件,可减少代码复杂度。
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
- 方式二,数据懒加载
通过 数据懒加载(LazyForEach) 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
LazyForEach(
dataSource: IDataSource,
itemGenerator: (item: any) => void,
keyGenerator?: (item: any) => string
): void
interface IDataSource {
totalCount(): number;
getData(index: number): any;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
interface DataChangeListener {
onDataReloaded(): void;
onDataAdd(index: number): void;
onDataMove(from: number, to: number): void;
onDataDelete(index: number): void;
onDataChange(index: number): void;
}
ForEach
ForEach循环渲染的过程如下:
- 从列表数据源一次性加载全量数据。
- 为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
- 列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。
其数据加载、组件树挂载、页面渲染的示意图如下所示:
图3ForEach渲染过程示意图
如果列表数据较少,数据一次性全量加载不是性能瓶颈时,可以直接使用ForEach;但是当数据量大、组件结构复杂的情况下ForEach会出现性能瓶颈。这是因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面启动时间过长。另外,屏幕可视区外的组件虽然不会显示在屏幕上,但是仍然会占用内存。在系统处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。
为了解决上述可能出现的问题,HarmonyOS应用框架进一步提供了懒加载方式 。
LazyForEach
LazyForEach懒加载的原理和渲染过程如下:
- LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。
- 根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。即,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
- 屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
其数据加载、组件树挂载、页面渲染的示意图如下所示:
图4LazyForEach渲染过程示意图
LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。不过在长列表滑动的过程中,因为需要根据用户的滑动行为不断地加载新的内容,这需要进行额外的数据请求和处理,会增加滑动时的计算量,从而对性能产生一定的影响。然而,合理使用LazyForEach的按需加载能力,通过在滑动停止或达到某个阈值时才进行加载,可以减少不必要的计算和请求,从而提高性能,给用户带来更好的体验。总之,在实现按需加载的场景中,需要综合考虑性能和用户体验的平衡,合理地优化加载逻辑和渲染方式,以提升整体的性能表现。
使用场景和规则
使用场景
上文了解到ForEach是从列表数据源一次性加载全量数据,且一次性并全部挂载在组件树上;LazyForEach是按需加载部分数据,只构建出一棵短小的组件树。针对数据加载和组件树构建这两个显著差异,可以对ForEach/LazyForEach做出如下选型判断:
表1 使用场景和选型原则
渲染接口 | 选型原则 |
---|---|
ForEach | 列表数据较少,数据一次性全量加载不是性能瓶颈时。ForEach相对LazyForEach,代码简单很多。 |
LazyForEach | 列表数据较长,一次性加载所有的列表数据创建、渲染页面产生性能瓶颈时。 |
场景案例
为了对比List组件在不同数据量下使用ForEach和LazyForEach的性能差异,可以