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  46 47\ 48 Swiper 共 5 页,当开发者设置了 cachedCount 属性为 1 且 loop 属性为 true 时,预加载的结果如下:\ 49  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