1# 避免在滑动场景的高频回调接口中处理耗时操作 2 3## 概述 4在滑动场景或者频繁创建和销毁组件等场景中,容易出现应用卡顿丢帧的问题。大多是由于场景中存在高频的接口调用,同时接口中执行了耗时操作,导致应用出现卡顿丢帧的现象,严重影响用户体验。为了帮助开发者优化应用性能,提升用户体验,本文将介绍以下四种需要避免处理耗时操作的高频场景: 5- **组件复用时避免在aboutToReuse中执行耗时操作。** 例如,在滑动场景中,使用组件复用通常需要用生命周期回调aboutToReuse去更新组件的状态变量。在滑动时,aboutToReuse会被频繁调用。如果在aboutToReuse中进行了耗时操作,将导致应用出现卡顿丢帧的问题。 6- **避免在aboutToAppear,aboutToDisappear中执行耗时操作。** 例如,在需要频繁创建和销毁组件的场景中,如果频繁在组件生命周期回调aboutToAppear,aboutToDisappear中执行耗时操作,会导致应用出现卡顿丢帧的问题。 7- **避免在LazyForEach的itemGenerator,keyGenerator,getData中执行耗时操作。** 例如,在懒加载滑动场景中,框架会根据滚动容器可视区域按需创建组件,所以在滑动时框架会频繁调用子组件生成函数itemGenerator,键值生成函数keyGenerator,获取索引数据函数getData。如果在itemGenerator,keyGenerator,getData中执行了耗时操作(比如传入耗时的函数作为入参),就会导致应用出现卡顿丢帧的问题。 8- **避免在组件的属性中执行耗时操作。** 在组件单一属性刷新时,组件的其他属性也会同时进行刷新。在需要频繁刷新组件属性的场景中,如果组件中其他不需要刷新的属性使用了耗时的函数作为入参。那么在刷新组件某个属性时,组件中那些实际上不需要去刷新的属性将会去调用耗时函数,导致不必要的性能损耗,同时也会引起应用卡顿丢帧的问题。 9 10## 组件复用时避免在aboutToReuse中执行耗时操作 11 12这里以[Grid懒加载组件复用场景](./grid_optimization.md#场景示例)为例,在aboutToReuse中加入测试日志,观察在滑动Grid时aboutToReuse的调用情况。 13```ts 14aboutToReuse(params: Record<string, number>) { 15 this.item = params.item; 16 console.log("Scenario 1 aboutToReuse"); 17} 18``` 19 20图1 滑动时的aboutToReuse日志 21 22 23 24如图1所示,从日志中可以看出,滑动时框架会频繁调用组件复用的aboutToReuse来更新节点。 25 26下面将基于这种组件复用时滑动会高频调用aboutToReuse的场景,在aboutToReuse中执行耗时操作和不执行耗时操作来分析正反例场景的性能差异。 27 28**反例:** 29 30在aboutToReuse中进行耗时操作。 31 32```ts 33... 34// 这里用循环函数模拟耗时操作 35count(): number { 36 let temp: number = 0; 37 for (let index = 0; index < 1000000; index++) { 38 temp += index; 39 } 40 return temp; 41} 42 43aboutToReuse(params: Record<string, number>) { 44 this.item = params.item; 45 // 模拟耗时操作 46 this.count(); 47} 48... 49``` 50 51**正例:** 52 53在aboutToReuse中不进行耗时操作。 54 55```ts 56... 57aboutToReuse(params: Record<string, number>) { 58 this.item = params.item; 59} 60... 61``` 62 63**效果对比** 64 65下面是使用SmartPerf工具抓取trace来分析滑动时在aboutToReuse中进行耗时操作和不进行耗时操作的性能差异。抓取trace前,需要先打开ArkUI节点树布局详细过程的trace开关,否则抓不到下面提到的trace标签“H:aboutToReuse ReusableChildComponent”。通过`hdc shell`进入命令行交互模式,执行`param set persist.ace.trace.layout.enabled true`命令打开。 66 67图2 反例滑动时单个aboutToReuse耗时 68 69 70 71图3 正例滑动时单个aboutToReuse耗时 72 73 74 75如图2所示,从反例trace中“H:aboutToReuse ReusableChildComponent”标签可以看出,单个aboutToReuse执行耗时21ms。而从图3正例trace中“H:aboutToReuse ReusableChildComponent”标签看,单个aboutToReuse执行耗时仅80μs。在高频调用aboutToReuse的场景中,如果每次调用aboutToReuse中都去执行耗时操作,将会导致应用性能大幅下降。因此,组件复用时应避免在aboutToReuse中执行耗时操作。 76 77 78## 避免在aboutToAppear,aboutToDisappear中执行耗时操作 79 80下面是一个使用条件渲染,通过点击按钮切换自定义组件A和B来模拟频繁创建和销毁组件的场景示例。在自定义组件A,B的生命周期回调函数aboutToAppear和aboutToDisappear中加入测试日志,用于观察点击按钮模拟频繁创建和销毁组件场景中的调用情况。 81```ts 82@Entry 83@Component 84struct Index { 85 // 切换自定义组件标志位 86 @State flag: boolean = false; 87 88 build() { 89 Column({ space: 10 }) { 90 Button('switch custom component').onClick(() => { 91 // 点击按钮切换自定义组件 92 this.flag = !this.flag; 93 }) 94 // 使用条件渲染,通过点击按钮来模拟频繁创建和销毁组件的场景 95 if (this.flag) { 96 // 自定义组件A 97 CustomComponentA() 98 } else { 99 // 自定义组件B 100 CustomComponentB() 101 } 102 }.width('100%').height('100%') 103 } 104} 105 106@Component 107struct CustomComponentA { 108 aboutToAppear() { 109 console.log("CustomComponentA aboutToAppear"); 110 } 111 112 aboutToDisappear() { 113 console.log("CustomComponentA aboutToDisappear"); 114 } 115 116 build() { 117 Column() { 118 }.backgroundColor(Color.Blue).width(200).height(200) 119 } 120} 121 122@Component 123struct CustomComponentB { 124 aboutToAppear() { 125 console.log("CustomComponentB aboutToAppear"); 126 } 127 128 aboutToDisappear() { 129 console.log("CustomComponentB aboutToDisappear"); 130 } 131 132 build() { 133 Column() { 134 }.backgroundColor(Color.Red).width(200).height(200) 135 } 136} 137``` 138 139图4 点击10次按钮的aboutToAppear和aboutToDisappear日志 140 141 142 143模拟频繁创建和销毁组件的场景,进行10次点击按钮切换自定义组件的操作。如图4所示,从日志中可以看出aboutToAppear和aboutToDisappear共调用了20次。因为示例中使用了条件渲染,每次销毁前一个自定义组件都会调用一次aboutToDisappear函数,然后创建新的自定义组件时,又会调用一次aboutToAppear,所以调用较为频繁。 144 145示例中只模拟了条件渲染时两个自定义组件间的频繁创建和销毁组件的场景,但是在实际复杂的应用业务中,可能需要对大量自定义组件进行频繁创建和销毁,因此会出现高频调用aboutToAppear和aboutToDisappear的情况。如果在aboutToAppear和aboutToDisappear中再去进行耗时操作(类似前面的“组件复用时避免在aboutToReuse中执行耗时操作”场景,这里就不再赘述),将会导致应用出现卡顿丢帧的问题。因此,在频繁创建和销毁组件的场景中,应避免在aboutToAppear,aboutToDisappear中执行耗时操作。 146 147 148## 避免在LazyForEach的itemGenerator,keyGenerator,getData中执行耗时操作 149 150这里还是以前面“组件复用时避免在aboutToReuse中执行耗时操作”场景中Grid懒加载组件复用场景为例,把itemGenerator,keyGenerator参数修改为函数入参itemGeneratorFunc,keyGeneratorFunc。同时在itemGeneratorFunc,keyGeneratorFunc,getData中加入测试日志,观察在懒加载Grid滑动时的调用情况。 151```ts 152// MyDataSource类实现IDataSource接口 153class MyDataSource implements IDataSource { 154 private dataArray: number[] = []; 155 156 public pushData(data: number): void { 157 this.dataArray.push(data); 158 } 159 160 // 数据源的数据总量 161 public totalCount(): number { 162 return this.dataArray.length; 163 } 164 165 // 返回指定索引位置的数据 166 public getData(index: number): number { 167 console.log("Scenario 3 getData,index value:" + this.dataArray[index]); 168 return this.dataArray[index]; 169 } 170 171 registerDataChangeListener(listener: DataChangeListener): void { 172 } 173 174 unregisterDataChangeListener(listener: DataChangeListener): void { 175 } 176} 177 178@Entry 179@Component 180struct MyComponent { 181 // 数据源 182 private data: MyDataSource = new MyDataSource(); 183 184 aboutToAppear() { 185 for (let i = 1; i < 1000; i++) { 186 this.data.pushData(i); 187 } 188 } 189 190 // itemGenerator入参函数 191 itemGeneratorFunc(item: number): number { 192 console.log("Scenario 3 itemGenerator,item:" + item); 193 return item; 194 } 195 196 // keyGenerator入参函数 197 keyGeneratorFunc(item: number): string { 198 console.log("Scenario 3 keyGenerator,item:" + item); 199 return JSON.stringify(item); 200 } 201 202 build() { 203 Column({ space: 5 }) { 204 Grid() { 205 LazyForEach(this.data, (item: number) => { 206 GridItem() { 207 // 使用可复用自定义组件 208 ReusableChildComponent({ item: this.itemGeneratorFunc(item) }) 209 } 210 }, (item: number) => this.keyGeneratorFunc(item)) 211 } 212 .cachedCount(2) // 设置GridItem的缓存数量 213 .columnsTemplate('1fr 1fr 1fr') 214 .columnsGap(10) 215 .rowsGap(10) 216 .margin(10) 217 .height(500) 218 .backgroundColor(0xFAEEE0) 219 } 220 } 221} 222 223// 自定义组件被@Reusable装饰器修饰,即标志其具备组件复用的能力 224@Reusable 225@Component 226struct ReusableChildComponent { 227 @State item: number = 0; 228 229 // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容 230 aboutToReuse(params: Record<string, number>) { 231 this.item = params.item; 232 } 233 234 build() { 235 Column() { 236 Image($r('app.media.icon')) 237 .objectFit(ImageFit.Fill) 238 .layoutWeight(1) 239 Text(`图片${this.item}`) 240 .fontSize(16) 241 .textAlign(TextAlign.Center) 242 } 243 .width('100%') 244 .height(120) 245 .backgroundColor(0xF9CF93) 246 } 247} 248``` 249 250图5 懒加载滑动Grid日志 251 252 253 254如图5所示,从日志中可以看出,在懒加载Grid滑动时,会频繁调用getData,itemGenerator,keyGenerator。因为滑动时框架会对比item键值,判断是使用缓存节点还是新建节点。因此会先调用getData获取索引位置的数据,并提供给keyGenerator去对比键值,如果需要新建节点就会去调用itemGenerator。因此,在懒加载滑动场景中,会频繁调用getData,itemGenerator,keyGenerator。如果滑动时在这些函数中执行耗时操作,将会导致应用出现卡顿丢帧的问题。 255 256下面是分别在itemGenerator,keyGenerator,getData中执行耗时操作和不执行耗时操作的正反例。由于真实场景的函数中可能存在未知的耗时操作逻辑,因此这里用循环函数模拟耗时操作。 257 258### 避免在LazyForEach的itemGenerator中执行耗时操作 259 260**反例:** 261 262在itemGenerator入参函数中执行耗时操作。 263 264```ts 265... 266itemGeneratorFunc(item: number): number { 267 // 这里用循环函数模拟耗时操作 268 let temp: number = 0; 269 for (let index = 0; index < 1000000; index++) { 270 temp += 1; 271 } 272 item += temp; 273 return item; 274} 275... 276Grid() { 277 LazyForEach(this.data, (item: number) => { 278 GridItem() { 279 // 传入耗时操作函数入参this.itemGeneratorFunc(item) 280 ReusableChildComponent({ item: this.itemGeneratorFunc(item) }) 281 } 282 }, (item: number) => JSON.stringify(item)) 283} 284... 285``` 286 287**正例:** 288 289在aboutToAppear中执行耗时操作。 290 291```ts 292// 耗时操作计算的值 293private timeConsumingValue: number = 0; 294 295aboutToAppear() { 296 ... 297 // 执行该异步函数 298 this.itemGeneratorFunc(); 299} 300 301// 这里用循环函数模拟耗时操作 302async itemGeneratorFunc() { 303 let temp: number = 0; 304 for (let index = 0; index < 1000000; index++) { 305 temp += 1; 306 } 307 this.timeConsumingValue = temp; 308} 309... 310Grid() { 311 LazyForEach(this.data, (item: number) => { 312 GridItem() { 313 // 传入耗时操作计算的值 314 ReusableChildComponent({ item: item + this.timeConsumingValue }) 315 } 316 }, (item: number) => JSON.stringify(item)) 317} 318... 319``` 320 321### 避免在LazyForEach的keyGenerator中执行耗时操作 322 323**反例:** 324 325在keyGenerator入参函数中执行耗时操作。 326 327```ts 328... 329keyGeneratorFunc(item: number): string { 330 // 这里用循环函数模拟耗时操作 331 let temp: number = 0; 332 for (let index = 0; index < 1000000; index++) { 333 temp += 1; 334 } 335 item += temp; 336 return JSON.stringify(item); 337} 338... 339Grid() { 340 LazyForEach(this.data, (item: number) => { 341 GridItem() { 342 ReusableChildComponent({ item: item }) 343 } 344 }, (item: number) => this.keyGeneratorFunc(item)) // 传入耗时操作函数入参this.keyGeneratorFunc(item) 345} 346... 347``` 348 349**正例:** 350 351在aboutToAppear中执行耗时操作。 352 353```ts 354// 耗时操作计算的值 355private timeConsumingValue: number = 0; 356 357aboutToAppear() { 358 ... 359 // 执行该异步函数 360 this.keyGeneratorFunc(); 361} 362 363// 这里用循环函数模拟耗时操作 364async keyGeneratorFunc() { 365 let temp: number = 0; 366 for (let index = 0; index < 1000000; index++) { 367 temp += 1; 368 } 369 this.timeConsumingValue = temp; 370} 371... 372Grid() { 373 LazyForEach(this.data, (item: number) => { 374 GridItem() { 375 // 使用可复用自定义组件 376 ReusableChildComponent({ item: item }) 377 } 378 }, (item: number) => JSON.stringify(item + this.timeConsumingValue)) 379} 380... 381``` 382 383### 避免在LazyForEach的getData中执行耗时操作 384 385**反例:** 386 387在getData中执行耗时操作。 388 389```ts 390... 391// 返回指定索引位置的数据 392public getData(index: number): number { 393 // 这里用循环函数模拟耗时操作 394 let temp: number = 0; 395 for (let index = 0; index < 1000000; index++) { 396 temp += 1; 397 } 398 return this.dataArray[index]; 399} 400... 401``` 402 403**正例:** 404 405避免在getData中执行耗时操作。 406 407```ts 408... 409// 返回指定索引位置的数据 410public getData(index: number): number { 411 // 不在getData中执行耗时操作 412 return this.dataArray[index]; 413} 414... 415``` 416 417**效果对比** 418 419由于上述itemGenerator,keyGenerator,getData滑动场景中正反例的对比效果类似,所以下面只给出itemGenerator正反例的效果对比。 420 421图6 itemGenerator中执行耗时操作的滑动效果 422 423 424 425图6是在itemGenerator入参函数中执行耗时操作的滑动效果,可以明显看出滑动时存在卡顿,item节点刷新慢等问题。 426 427图7 itemGenerator中不执行耗时操作的滑动效果 428 429 430 431图7是在aboutToAppear中执行耗时操作,把耗时操作计算的值timeConsumingValue传入itemGenerator的滑动效果,可以看出滑动效果流畅,无卡顿问题。 432 433因此,在懒加载滑动场景中,应避免在LazyForEach的itemGenerator,keyGenerator,getData中执行耗时操作,可以有效减少应用卡顿丢帧的问题,提升用户体验。 434 435## 避免在组件的属性中执行耗时操作 436 437下面是一个点击按钮改变Row组件宽度的示例,Row的高度使用getHeight()函数入参形式调用。在getHeight()中加入测试日志,用于观察修改Row宽度时,Row高度属性入参函数的调用情况。 438```ts 439@Entry 440@Component 441struct Index { 442 // Row宽度 443 @State rowWidth: number = 100; 444 // 点击按钮改变Row宽度的次数 445 private count: number = 0; 446 447 // 获取Row组件高度 448 getHeight(): number { 449 console.log("Scenario 4 call getHeight"); 450 return 100; 451 } 452 453 build() { 454 Column({ space: 10 }) { 455 // 点击按钮修改Row组件宽度 456 Button('change row width').onClick(() => { 457 this.rowWidth = this.rowWidth + 20; 458 this.count++; 459 console.log("Scenario 4 change row width count:" + this.count); 460 if (this.rowWidth > 200) { 461 this.rowWidth = 100; 462 } 463 }) 464 Row().width(this.rowWidth).height(this.getHeight()).backgroundColor(Color.Blue) 465 }.width('100%').height('100%') 466 } 467} 468``` 469 470图8 点击按钮改变Row宽度日志 471 472 473 474如图8所示,从日志中可以看出,每次点击按钮改变Row组件宽度时,Row的高度也会同时刷新。由此可见,在组件单一属性刷新时,组件的其他属性也会同时进行刷新。因此,在高频刷新组件属性的场景中,将会频繁调用组件所有属性的刷新。 475 476下面通过在组件属性中使用耗时的函数入参调用作为反例,使用[任务池taskpool](../reference/apis-arkts/js-apis-taskpool.md)处理耗时操作后返回结果给Row的高度rowHeight作为正例,来对比组件属性刷新时的性能差异。 477 478**反例:** 479 480在getHeight()中添加耗时操作作为反例。 481 482```ts 483... 484 // 获取Row组件高度 485 getHeight(): number { 486 let height: number = 0; 487 // 这里用循环函数模拟耗时操作 488 for (let index = 0; index < 1000000; index++) { 489 height += 0.0001; 490 } 491 console.log("Scenario 4 call getHeight"); 492 return height; 493 } 494... 495``` 496 497**正例:** 498 499使用任务池taskpool处理耗时操作后返回结果给Row的高度rowHeight作为正例。 500 501```ts 502import taskpool from '@ohos.taskpool'; // 任务池 503 504@Concurrent 505function getHeight(): number { 506 let height: number = 0; 507 // 这里用循环函数模拟耗时操作 508 for (let index = 0; index < 1000000; index++) { 509 height += 0.0001; 510 } 511 console.log("Scenario 4 call getHeight"); 512 return height; 513} 514 515// 执行getHeight() 516taskpool.execute(getHeight).then((value: Object) => { 517 AppStorage.setOrCreate('height', value); 518}); 519 520@Entry 521@Component 522struct Index { 523 // Row宽度 524 @State rowWidth: number = 100; 525 // Row高度 526 @StorageLink('height') rowHeight: number = 0; 527 // 点击按钮改变Row宽度的次数 528 private count: number = 0; 529 530 build() { 531 Column({ space: 10 }) { 532 Button('change row width').onClick(() => { 533 this.rowWidth = this.rowWidth + 20; 534 this.count++; 535 console.log("Scenario 4 change row width count:" + this.count); 536 if (this.rowWidth > 200) { 537 this.rowWidth = 100; 538 } 539 }) 540 Row().width(this.rowWidth).height(this.rowHeight).backgroundColor(Color.Blue) 541 }.width('100%').height('100%') 542 } 543} 544``` 545 546**效果对比** 547 548图9 反例改变Row组件宽度日志 549 550 551 552如图9所示,从日志可以看出,每次改变Row组件宽度rowWidth,都会调用一次耗时的Row高度入参函数getHeight()。 553 554图10 正例改变Row组件宽度日志 555 556 557 558如图10所示,从日志可以看出,页面加载时通过taskpool方式仅执行一次耗时的getHeight()。然后返回结果直接赋值给Row高度变量rowHeight。修改6次Row组件宽度,不需要再重复调用耗时的getHeight(),有效减少了不必要的性能损耗。 559 560因此,在高频刷新组件属性的场景中,应避免在组件的属性中执行耗时操作(如属性使用耗时的函数入参),能有效减少应用卡顿丢帧的情况,提升用户体验。