• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Swiper高性能开发指导
2
3<!--Kit: Common-->
4<!--Subsystem: Demo&Sample-->
5<!--Owner: @mgy917-->
6<!--Designer: @jiangwensai-->
7<!--Tester: @Lyuxin-->
8<!--Adviser: @huipeizi-->
9
10## 背景
11
12在应用开发中,[Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md) 组件常用于翻页场景,比如:桌面、图库等应用。Swiper 组件滑动切换页面时,基于按需加载原则通常会在下一个页面将要显示时才对该页面进行加载和布局绘制,这个过程包括:
13
14- 如果该页面使用了@Component 装饰的自定义组件,那么自定义组件的 build 函数会被执行并创建内部的 UI 组件;
15
16- 如果使用了[LazyForEach](../ui/state-management/arkts-rendering-control-lazyforeach.md),会执行 LazyForEach 的 UI 生成函数生成 UI 组件;
17
18- 在 UI 组件构建完成后,会对 UI 组件进行布局测算和绘制。
19
20针对复杂页面场景,该过程可能会持续较长时间,导致滑动过程中出现卡顿,对滑动体验造成负面影响,甚至成为整个应用的性能瓶颈。如在图库大图浏览场景中,若不使用预加载机制,每次都将在滑动开始的首帧去加载下一张图片,会导致首帧耗时过长甚至掉帧,拖慢应用性能。
21
22为了解决上述问题,可以使用 Swiper 组件的预加载机制,利用主线程的空闲时间来提前构建和布局绘制组件,优化滑动体验。
23
24## 使用场景
25
26如果开发者的应用场景属于加载较为耗时的场景时,尤其是下列场景,推荐使用 Swiper 预加载功能。
27
28- Swiper 的子组件大于等于五个;
29
30- Swiper 的子组件具有复杂的动画;
31
32- Swiper 的子组件加载时需要执行网络请求等耗时操作;
33
34- Swiper 的子组件包含大量需要渲染的图像或资源。
35
36## Swiper 预加载机制说明
37
38预加载机制是 Swiper 组件中一个重要的特性,允许 Swiper 滑动到下一个子组件之前提前加载后续页面的内容,其主要目的是提高应用滑动时的流畅性和响应速度。当用户尝试滑动到下一个子组件时,如果下一个子组件的内容已经提前加载完毕,那么滑动就会立即发生,否则 Swiper 组件需要在加载下一个子组件的同时处理滑动事件,对滑动体验造成负面影响。当前 Swiper 组件的预加载在用户滑动离手动效开始时触发,离手动效的计算在渲染线程中进行,因此主线程有空闲的时间可以进行预加载的操作。配合 LazyForEach 的按需加载和销毁能力,可以在优化滑动体验基础上节省内存占用。
39
40## 使用指导
41
42- 预加载子组件的个数在[cachedCount](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#cachedcount8)属性中配置。
43
44Swiper 共 5 页,当开发者设置了 cachedCount 属性为 1 且 loop 属性为 false 时,预加载的结果如下:\
45 ![loop=false](figures/swiper_loop_false.png)
46
47\
48 Swiper 共 5 页,当开发者设置了 cachedCount 属性为 1 且 loop 属性为 true 时,预加载的结果如下:\
49 ![loop=true](figures/swiper_loop_true.png)
50
51- Swiper 组件的子组件使用[LazyForEach](../ui/state-management/arkts-rendering-control-lazyforeach.md)动态加载和销毁组件。
52
53**示例**
54
55```TypeScript
56class MyDataSource implements IDataSource { // LazyForEach的数据源
57  private list: number[] = [];
58
59  constructor(list: number[]) {
60    this.list = list;
61  }
62
63  totalCount(): number {
64    return this.list.length;
65  }
66
67  getData(index: number): number {
68    return this.list[index];
69  }
70
71  registerDataChangeListener(_: DataChangeListener): void {
72  }
73
74  unregisterDataChangeListener(): void {
75  }
76}
77
78@Component
79struct SwiperChildPage { // Swiper的子组件
80  @State arr: number[] = [];
81
82  aboutToAppear(): void {
83    for (let i = 1; i <= 100; i++) {
84      this.arr.push(i);
85    }
86  }
87
88  build() {
89    Column() {
90      List({ space: 20 }) {
91        ForEach(this.arr, (index: number) => {
92          ListItem() {
93            Text(index.toString())
94              .height('4.5%')
95              .fontSize(16)
96              .textAlign(TextAlign.Center)
97              .backgroundColor(0xFFFFFF)
98          }
99          .border({ width: 2, color: Color.Green })
100        }, (index: number) => index.toString());
101      }
102      .height("95%")
103      .width("95%")
104      .border({ width: 3, color: Color.Red })
105      .lanes({ minLength: 40, maxLength: 40 })
106      .alignListItem(ListItemAlign.Start)
107      .scrollBar(BarState.Off)
108
109    }.width('100%').height('100%').padding({ top: 5 });
110  }
111}
112
113@Entry
114@Preview
115@Component
116struct SwiperExample {
117  private dataSrc: MyDataSource = new MyDataSource([]);
118
119  aboutToAppear(): void {
120    let list: Array<number> = [];
121    for (let i = 1; i <= 10; i++) {
122      list.push(i);
123    }
124    this.dataSrc = new MyDataSource(list);
125  }
126
127  build() {
128    Column({ space: 5 }) {
129      Swiper() {
130        LazyForEach(this.dataSrc, (_: number) => {
131          SwiperChildPage();
132        }, (item: number) => item.toString());
133      }
134      .loop(false)
135      .cachedCount(1) // 提前加载后一项的内容
136      .indicator(true)
137      .duration(100)
138      .displayArrow({
139        showBackground: true,
140        isSidebarMiddle: true,
141        backgroundSize: 40,
142        backgroundColor: Color.Orange,
143        arrowSize: 25,
144        arrowColor: Color.Black
145      }, false)
146      .curve(Curve.Linear)
147
148    }.width('100%')
149    .margin({ top: 5 })
150  }
151}
152
153```
154
155## 验证效果
156
157为了更好地体现 Swiper 预加载机制带来的性能优化效果,用例采用下列前置条件:
158
159- Swiper 的子组件为带有 100 个 ListItem 的 List 组件;
160
161- Swiper 组件共有 10 个 List 子组件。
162
163在该场景下,使用 Swiper 预加载机制可以为每个翻页动作节省约40%的时间,同时保证翻页时不丢帧,保证翻页的流畅度。
164
165## 优化建议
166
167由于组件构建和布局计算需要一定时间,cachedCount 的数量也不是设置得越大越好,过大的 cachedCount 可能会导致应用性能降低。当前 Swiper 组件滑动离手后的动效时间大约是 400ms,如果应用加载一个子组件的时间在 100ms\~200ms 之间,为了在离手动效时间内完成组件的预加载,cachedCount 属性建议设置为 1 或 2,设置过大会导致主线程阻塞而产生卡顿。
168
169那么方案可以继续优化,在抛滑场景时,Swiper 组件有一个[OnAnimationStart](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#事件)回调接口,切换动画开始时触发该回调。此时,主线程空闲,应用可以充分利用这段时间进行图片等资源的预加载,减少后续 cachedCount 范围内的节点预加载耗时;
170跟手滑动阶段不会触发[OnAnimationStart](../reference/apis-arkui/arkui-ts/ts-container-swiper.md#事件)回调,只有在离手后做切换动画(也就是抛滑阶段)才会触发。
171
172**示例**
173
174Swiper 子组件页面代码如下:
175
176在子组件首次构建(生命周期执行到[aboutToAppear](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear))时,先判断 dataSource 中该 index 的数据是否有数据,若无数据则先进行资源加载,再构建节点。若有数据,则直接构建节点即可。
177
178```TypeScript
179import { image } from "@kit.ImageKit";
180import { MyDataSource } from './Index';
181
182@Component
183export struct PhotoItem { //Swiper的子组件
184  myIndex: number = 0;
185  private dataSource: MyDataSource = new MyDataSource([]);
186  context = this.getUIContext().getHostContext();
187  @State imageContent: image.PixelMap | undefined = undefined;
188
189  aboutToAppear(): void {
190    console.info(`aboutToAppear` + this.myIndex);
191    this.imageContent = this.dataSource.getData(this.myIndex)?.image;
192    if (!this.imageContent) { // 先判断dataSource中该index的数据是否有数据,若无数据则先进行资源加载
193      try {
194        // 获取resourceManager资源管理器
195        const resourceMgr = this.context?.resourceManager;
196        // 获取rawfile文件夹下item.jpg的ArrayBuffer
197        let str = "item" + (this.myIndex + 1) + ".jpg";
198        resourceMgr?.getRawFileContent(str).then((value) => {
199          // 创建imageSource
200          const imageSource = image.createImageSource(value.buffer);
201          imageSource.createPixelMap().then((value) => {
202            console.info("aboutToAppear push" + this.myIndex);
203            this.dataSource.addData(this.myIndex, { description: "" + this.myIndex, image: value });
204            this.imageContent = value;
205          })
206        })
207      } catch (err) {
208        console.error("error code" + err);
209      }
210    }
211  }
212
213  build() {
214    Column() {
215      Image(this.imageContent)
216        .width("100%")
217        .height("100%")
218    }
219  }
220}
221```
222
223Swiper 主页面的代码如下:
224```TypeScript
225import { curves } from "@kit.ArkUI";
226import { PhotoItem } from './PhotoItem';
227import { image } from "@kit.ImageKit";
228
229interface MyObject {
230  description: string,
231  image: image.PixelMap,
232}
233
234export class MyDataSource implements IDataSource {
235  private list: MyObject[] = [];
236
237  constructor(list: MyObject[]) {
238    this.list = list;
239  }
240
241  totalCount(): number {
242    return this.list.length;
243  }
244
245  getData(index: number): MyObject {
246    return this.list[index];
247  }
248
249  registerDataChangeListener(listener: DataChangeListener): void {
250  }
251
252  unregisterDataChangeListener(listener: DataChangeListener): void {
253  }
254
255  addData(index: number, data: MyObject) {
256    this.list[index] = data;
257  }
258}
259
260@Entry
261@Component
262struct Index {
263  @State currentIndex: number = 0;
264  cacheCount: number = 1;
265  swiperController: SwiperController = new SwiperController();
266  private data: MyDataSource = new MyDataSource([]);
267  context = this.getUIContext().getHostContext();
268
269  aboutToAppear() {
270    let list: MyObject[] = [];
271    for (let i = 0; i < 6; i++) {
272      list.push({ description: "", image: this.data.getData(this.currentIndex)?.image });
273    }
274    this.data = new MyDataSource(list);
275  }
276
277  build() {
278    Swiper(this.swiperController) {
279      LazyForEach(this.data, (item: MyObject, index?: number) => {
280        PhotoItem({
281          myIndex: index,
282          dataSource: this.data
283        })
284      })
285    }
286    .cachedCount(this.cacheCount)
287    .curve(curves.interpolatingSpring(0, 1, 228, 30))
288    .index(this.currentIndex)
289    .indicator(true)
290    .loop(false)
291    // 在OnAnimationStart接口回调中进行预加载资源的操作
292    .onAnimationStart((index: number, targetIndex: number) => {
293      console.info("onAnimationStart " + index + " " + targetIndex);
294      if (targetIndex !== index) {
295        try {
296          // 获取resourceManager资源管理器
297          const resourceMgr = this.context?.resourceManager;
298          // 获取rawfile文件夹下item.jpg的ArrayBuffer
299          let str = "item" + (targetIndex + this.cacheCount + 2) + ".jpg";
300          resourceMgr?.getRawFileContent(str).then((value) => {
301            // 创建imageSource
302            const imageSource = image.createImageSource(value.buffer);
303            imageSource.createPixelMap().then((value) => {
304              this.data.addData(targetIndex + this.cacheCount + 1, {
305                description: "" + (targetIndex + this.cacheCount + 1),
306                image: value
307              });
308            });
309          })
310        } catch (err) {
311          console.error("error code" + err);
312        }
313      }
314    })
315    .width('100%')
316    .height('100%')
317  }
318}
319```
320
321## 总结
322
323- Swiper 组件的预加载机制与 LazyForEach 结合使用,能够达到最佳优化效果。
324
325- 预加载的 cachedCount 并非越大越好,需要结合单个子组件加载耗时来设置。假设一个子组件的加载耗时为 Nms,那么 cachedCount 推荐设置为小于 400/N。
326
327- 如果应用有非常高的性能优化需求,Swiper 预加载机制可搭配 OnAnimationStart 接口回调使用,进一步提升预加载的效率。
328