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  39 40\ 41 Swiper 共 5 页,当开发者设置了 cacheCount 属性为 1 且 loop 属性为 true 时,预加载的结果如下:\ 42  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