概述
在应用开发中,Swiper组件常用于翻页场景,比如:桌面、图库等应用。Swiper组件滑动切换页面时,基于按需加载原则通常会在下一个页面将要显示时才对该页面进行加载和布局绘制。针对复杂页面场景,该过程可能会持续较长时间,导致滑动过程中出现卡顿,对滑动体验造成负面影响,甚至成为整个应用的性能瓶颈。
本文主要介绍Swiper性能优化的相关方法。
懒加载
原理介绍
懒加载LazyForEach从提供的数据源按需迭代数据,并在每次迭代过程中创建相应的组件,Swiper采用LazyForEach进行数据懒加载,在布局时会根据可视区域按需创建Swiper子组件,并在Swiper子组件滑出可视区域外时销毁以降低内存占用。Swiper组件的开发,属于滑动容器加载的一种场景
场景案例
为了体现Swiper使用ForEach与LazyForEach加载的性能差距,本地模拟答题场景进行测试分析。
Swiper子组件核心代码如下
@Component
struct SwiperItem {
//题干
private questionStr: string = '';
//题目相关的图片资源
private image: string | PixelMap = '';
//答案选项
private answerStr: string[] = [];
//当前题号
private myIndex: number = 0;
//构造数据
aboutToAppear(): void {
// 初始化题干、图片链接、答案选项
// ...
}
build() {
Column() {
// 题干
Text(this.questionStr)
.fontSize(26)
.width('100%')
// 题目相关图片
Image(this.image)
.width('100%')
.objectFit(ImageFit.Contain)
.margin({ top: 12, bottom: 12 })
// 答案
ForEach(this.answerStr, (item: string, index: number) => {
Text(item)
.width('100%')
.fontSize(26)
})
}
// ...
}
}
Swiper主页面核心代码如下
- 使用ForEach对页面进行加载
aboutToAppear(): void {
for (let i = 0; i < 1000; i++) {
this.list.push(i);
this.data.addData(i, i);
}
}
build() {
Column() {
Swiper(this.swiperController) {
ForEach(this.list, (item: number, index: number) => {
SwiperItem({ myIndex: index })
.width('100%')
.height("100%")
}, (item: string) => item)
}
// ...
}
.width('100%')
.margin({ top: 5 })
}
- 使用LazyForEach对页面进行加载
aboutToAppear(): void {
for (let i = 0; i < 1000; i++) {
this.data.addData(i, i);
}
}
build() {
Column() {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string, index: number) => {
SwiperItem({ myIndex: index })
.width('100%')
.height("100%")
}, (item: string) => item)
}
// ...
}
.width('100%')
.margin({ top: 5 })
}
加载方式 | 完全显示所用时间 | 丢帧率 | 独占内存 |
---|---|---|---|
ForEach | 951ms | 8.5% | 200MB |
LazyForEach | 280.6ms | 0.0% | 25.18MB |
由实验数据可知,当Swiper的子组件数量比较大时,采用懒加载可以带来较好的帧率提升,并且有效减低内存占用。
缓存数据项
原理介绍
LazyForEach懒加载可以通过设置[cachedCount]来指定缓存数量
使用场景
如果开发者的应用场景属于加载较为耗时的场景时,尤其是下列场景,推荐使用。
- Swiper的子组件具有复杂的动画;
- Swiper的子组件加载时需要执行网络请求等耗时操作;
- Swiper的子组件包含大量需要渲染的图像或资源。
场景案例
案例模拟Swiper的子组件包含大量图像资源,采用下列前置条件:
- Swiper的子组件为带有50个ListItem的List组件;
- 每个ListItem加载网络图片;
- Swiper组件共有20个List子组件;
- 一屏显示一个Swiper子组件。
Swiper子组件核心代码如下
@Component
struct SwiperItem {
private data: number[] = [];
private myIndex: number = 0;
// 构造数据
private imgURL: string[] = Constant.imgURL;
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.data.push(i);
}
}
build() {
Column() {
List({ space: 20 }) {
ForEach(this.data, (index: number) => {
ListItem() {
Image(this.imgURL[this.myIndex * 50 + index])
.objectFit(ImageFit.Contain)
.width("100%")
.height("100%")
}
.aspectRatio(1)
.border({ width: 2, color: Color.Green })
}, (index: number) => index.toString());
}
// ...
}
// ...
}
}
Swiper主页面核心代码如下
private dataSrc: NumberDataSource = new NumberDataSource();
aboutToAppear(): void {
for (let i = 0; i < 20; i++) {
this.dataSrc.addData(i, i);
}
}
build() {
Column({ space: 5 }) {
Swiper() {
LazyForEach(this.dataSrc, (item: number, index: number) => {
SwiperItem({
myIndex: index
});
}, (item: number) => item.toString());
}
.cachedCount(1)
.autoPlay(true)
.interval(1000)
.duration(100)
// ...
}.width('100%')
.margin({ top: 5 })
}
为测试不同缓存数量对性能的影响,将cachedCount的值分别设为1、2、4、8。基于案例程序,测试不同缓存数量对帧率以及内存占用的影响情况。
缓存数量 | 1 | 2 | 4 | 8 |
---|---|---|---|---|
丢帧率 | 3.0% | 3.3% | 3.1% | 3.0% |
独占内存 | 64.36MB | 117.39MB | 214.32MB | 377.38MB |
根据测试结果可知,随着cachedCount的增大,应用的内存占用呈线性增长,但帧率没有得到明显提升。
一般而言,一屏显示一个Swiper子组件的连续滑动场景,cachedCount值设为1或2即可。
说明
缓存数量仅供参考,不同的应用程序设置的最佳缓存数量不一致,需要针对应用程序测试得出最佳缓存数量。
提前加载数据
在抛滑场景时,Swiper组件有个[onAnimationStart]回调接口,切换动画开始时触发该回调。此时,切换动画的相关逻辑在渲染线程中进行,处于空闲状态的主线程便可以充分利用这段时间加载子组件所需的资源。例如图像,网络资源等,减少后续cachedCount范围内的节点预加载耗时;跟手滑动阶段不会触发onAnimationStart回调,只有在离手后做切换动画(也就是抛滑阶段)才会触发。
场景案例
Swiper子组件:在子组件首次构建(生命周期执行到[aboutToAppear])时,先判断datasource中该index的数据是否完备,若数据不完备则先进行资源加载,再构建节点。
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
@Component
struct SwiperItem {
@State private pixelMapArr: PixelMap[] = [];
private dataSource: PixelMapDataSource = new PixelMapDataSource();
private myIndex = 0;
private list: number[] = [];
aboutToAppear(): void {
hiTraceMeter.startTrace("标签xxxx", 1);
this.pixelMapArr = this.dataSource.getData(this.myIndex);
for (let i = 0; i < 50; i++) {
this.list.push(i);
// 如果当前index图片未进行加载 则加载图片资源
if (this.pixelMapArr.length <= i) {
Utils.httpRequest( this.pixelMapArr,(img:PixelMap,arr:PixelMap[])=>{
arr.push(img);
});
}
}
}
onDidBuild(): void {
hiTraceMeter.finishTrace("标签xxxx", 1);
}
build() {
Column() {
List({ space: 20 }) {
ForEach(this.list, (index: number) => {
ListItem() {
Image(this.pixelMapArr[index])
.objectFit(ImageFit.Contain)
.width("100%")
.height("100%")
}
.aspectRatio(1)
.border({ width: 2, color: Color.Green })
}, (index: number) => index.toString());
}
// ...
}
// ...
}
说明
打点事件说明,当SwiperItem发生预加载时,会先进入[自定义组件生命周期]回调aboutToAppear,在aboutToAppear回调中使用startTrace开启打点跟踪,随后会进入build渲染组件,build函数执行完成后进入onDidBuild回调,在该回调中使用finishTrace停止打点追踪。分别使用“noPreLoadData”,“preLoadData”标签统计两种场景下的SwiperItem预加载耗时,关于本例中使用性能打点的介绍
Swiper主页面
- 不提前加载数据
@Entry
@Component
struct NoPreLoadData {
private dataSrc: PixelMapDataSource = new PixelMapDataSource();
aboutToAppear(): void {
for (let i = 0; i < 20; i++) {
this.dataSrc.addData(i, []);
}
}
build() {
Column({ space: 5 }) {
Swiper() {
LazyForEach(this.dataSrc, (item: PixelMap[], index: number) => {
SwiperItem({
myIndex: index,
dataSource: this.dataSrc
});
}, (item: number, index: number) => index.toString());
}
// ...
}
.width('100%')
.margin({ top: 5 })
}
}
- 提前加载数据
@Entry
@Component
struct PreLoadData {
private dataSrc: PixelMapDataSource = new PixelMapDataSource();
aboutToAppear(): void {
for (let i = 0; i < 20; i++) {
this.dataSrc.addData(i, []);
}
}
build() {
Column({ space: 5 }) {
Swiper() {
LazyForEach(this.dataSrc, (item: PixelMap[], index: number) => {
SwiperItem({
myIndex: index,
dataSource: this.dataSrc
});
}, (item: number, index: number) => index.toString());
}
// ...
.onAnimationStart((index: number, targetIndex: number) => {
if (targetIndex != index) {
let srcArr = this.dataSrc.getData(targetIndex + 2);
for (let i = 0; i < 20; i++) {
// 提前加载网络图片
Utils.httpRequest(srcArr, (img: PixelMap, arr: PixelMap[]) => {
arr.push(img);
});
}
}
})
}
.width('100%')
.margin({ top: 5 })
}
}
性能分析:
如图1所示,不使用onAnimationStart回调提前加载数据,通过自定义打点标签“H:noPreLoadData”,可以看出SwiperItem节点的构建耗时50ms左右。
图1 没有提前加载数据的打点信息
如图2所示,采用onAnimationStart回调提前加载数据,通过自定义打点标签“H:preLoadData”,可以看出SwiperItem节点的构建耗时2ms左右。
图2 使用了提前加载数据的打点信息
观察“H:noPreLoadData”时间段的详细trace图,可以发现预加载构建SwiperItem时,aboutToAppear生命周期回调加载图片资源占用了48ms左右。
图3 “H:noPreLoadData”时间段的trace详细信息
通过上述分析可以发现,使用onAnimationStart回调接口提前加载后续范围内子组件所需资源,可以减小后续cachedCount范围内子组件节点的加载耗时。
组件复用
考虑到Swiper翻页场景存在其子组件的频繁创建和销毁,可以将子组件封装成自定义组件,并使用@Reusable装饰器修饰,使其具备组件复用能力,减少ArkUI框架内部反复创建销毁节点的开销。
总结
本文主要介绍了Swiper针对在复杂页面场景下,一些注意事项和性能优化方法。
- 使用LazyForEach懒加载,针对较多数据可以快速渲染显示,同时减少内存占用。
- 合理设置cachedCount缓存数据项,预先加载屏幕可视区外的数据缓存。
- 在抛滑场景中,可以利用渲染线程和主线程分离,在onAnimationStart接口回调中提前加载子组件所需资源,减小后续预加载所需时间。
- 使用@Reusable复用组件,借助RecycleManager从复用池中取出的视图对象,快速绑定新的数据,减少创建、销毁视图带来的性能损耗。