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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_01.png)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_02.png)
70
71图3 正例滑动时单个aboutToReuse耗时
72
73![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_03.png)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_04.png)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_05.png)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_06.gif)
424
425图6是在itemGenerator入参函数中执行耗时操作的滑动效果,可以明显看出滑动时存在卡顿,item节点刷新慢等问题。
426
427图7 itemGenerator中不执行耗时操作的滑动效果
428
429![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_07.gif)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_08.png)
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![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_09.png)
551
552如图9所示,从日志可以看出,每次改变Row组件宽度rowWidth,都会调用一次耗时的Row高度入参函数getHeight()。
553
554图10 正例改变Row组件宽度日志
555
556![](./figures/avoid_high_frequency_callback_execute_lengthy_operation_10.png)
557
558如图10所示,从日志可以看出,页面加载时通过taskpool方式仅执行一次耗时的getHeight()。然后返回结果直接赋值给Row高度变量rowHeight。修改6次Row组件宽度,不需要再重复调用耗时的getHeight(),有效减少了不必要的性能损耗。
559
560因此,在高频刷新组件属性的场景中,应避免在组件的属性中执行耗时操作(如属性使用耗时的函数入参),能有效减少应用卡顿丢帧的情况,提升用户体验。