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