在应用的UI开发中,使用列表是一种常规场景,因此,对列表性能进行优化是非常重要的。本文将针对应用开发列表场景的性能提升实践方法展开介绍。
简介
本文会介绍开发列表场景时的4种推荐优化方法,通过独立使用或组合使用这些优化方法,可以获得在启动时间、内存和系统资源方面的平衡,提升性能和用户体验。
懒加载:提供列表数据按需加载能力,解决一次性加载长列表数据耗时长、占用过多资源的问题,可以提升页面响应速度。
缓存列表项:提供屏幕可视区域外列表项长度的自定义调节能力,配合懒加载设置可缓存列表项参数,通过预加载数据提升列表滑动体验。
组件复用:提供可复用组件对象的缓存资源池,通过重复利用已经创建过并缓存的组件对象,降低组件短时间内频繁创建和销毁的开销,提升组件渲染效率。
布局优化:使用扁平化布局方案,减少视图嵌套层级和组件数,避免过度绘制,提升页面渲染效率。
懒加载
原理机制
应用框架为容器类组件的数据加载和渲染提供了2种方式:
方式1,提供ForEach实现一次性加载全量数据并循环渲染。
ForEach(
arr: any[], // 需要进行数据迭代的列表数组
itemGenerator: (item: any, index?: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index?: number) => string // (可选)键值生成函数
)
方式2,提供LazyForEach实现延迟加载数据并按需渲染。
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any) => void, // 子组件生成函数
keyGenerator?: (item: any) => string // (可选) 键值生成函数
)
ForEach循环渲染的过程如下:
- 从列表数据源一次性加载全量数据。
- 为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
- 列表内容显示时,只渲染屏幕可视区内的ListItem组件。可视区外的ListItem组件滑动进入屏幕内时,因为已经完成数据加载和组件创建挂载,直接渲染即可。
ForEach循环渲染在列表数据量大、组件结构复杂的情况下,会出现性能瓶颈。因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面启动时间过长。另外,屏幕可视区外的组件虽然不会显示在屏幕上,但是仍然会占用内存。在系统处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。
为了规避上述可能出现的问题,应用框架进一步提供了懒加载方式 。
LazyForEach懒加载的原理如下:
- LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。
- 并根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。即,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
- 屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。可以显著提升页面的能效比和用户体验。
使用场景和限制
如果列表数据较长,一次性加载所有的列表数据创建、渲染页面产生性能瓶颈时,开发者应该考虑使用数据LazyForEach懒加载。
如果列表数据较少,数据一次性全量加载不是性能瓶颈时,可以直接使用ForEach。
限制:ForEach、LazyForEach必须在List、Grid以及Swiper等容器组件内使用,用于循环渲染具有相同布局的子组件。更多懒加载的信息,请参考官方资料LazyForEach:数据懒加载。
LazyForEach懒加载API提供了cachedCount属性,用于配置可缓存列表项数量。除默认加载界面可视部分外,还可以加载屏幕可视区外指定数量(cachedCount)的缓存数据,详见下面“缓存列表项”章节。
实现示例
在List、Grid等容器组件下使用LazyForEach懒加载的示意代码如下:
// LazyForEach要遍历的数据源,为实现接口IDataSource的实例
private dataList = ...
build() {
Column() {
List() {
LazyForEach(this.dataList, // 数据源
(item: ListItemData) => {
// 根据列表项数据生成对应的组件
ListItem() {
this.initItem(item)
}
},(item: ListItemData) => item.itemId) // 生成列表项键值
}
}
}
接下来将结合示例代码,详细介绍LazyForEach懒加载的实现过程,包含下图所示的三部分内容:
1、准备数据源类
2、遍历数据源创建列表组件项
3、为列表项指定唯一的键值编码
代码实现如下。首先,在使用LazyForEach数据懒加载之前,需要实现懒加载数据源接口类IDataSource。数据源接口类提供了获取数据总量,返回指定索引位置的数据,以及注册、注销数据监听器的接口。编写一个实现数据源接口IDataSource的数据源类BasicDataSource,该类包含数据变更监听器DataChangeListener类型的实例变量listeners,用于维护注册的数据变更监听器,在数据变更时调用相应的回调函数。每一个listener实例对应一个ArkUI框架侧的LazyForEach实例,数据源数据发生变更时,listener实例会通知LazyForEach需要触发界面刷新。
BasicDataSource是一个抽象类,不同的具体列表页面的数据源需要根据业务场景分别实现该抽象类。以聊天列表场景为例,数据源具体类ChatListData实现如下。其中,列表项数组变量chatList: Array用于为List子组件提供数据。ChatModel类表示聊天列表中列表项,包含联系人信息、最后一条消息内容、时间戳、未读消息数量等信息;totalCount()和getData(index: number)是实现数据源接口类IDataSource中定义的方法,用于给LazyForEach提供数据,应用框架会调用这些方法;addData()和pushData()方法为数据源类中定义的方法,可用于给数据源增加数据。需要注意的是,在这2个方法中需要调用notifyDataAdd方法,用于调用DataChangeListener中的接口来触发LazyForEach刷新。
class ChatListData extends BasicDataSource {
/**
* 聊天列表项数组
*/
private chatList: Array<ChatModel> = []
/**
* 数据源的数据总量
*/
public totalCount(): number {
return this.chatList.length
}
/**
* 返回指定索引位置的数据
*/
public getData(index: number): ChatModel {
return this.chatList[index]
}
/**
* 指定位置添加一条聊天列表数据
*/
public addData(index: number, data: ChatModel): void {
this.chatList.splice(index, 0