1# 提升应用响应速度
2
3应用对用户的输入需要快速反馈,以提升交互体验,因此本文提供了以下方法来提升应用响应速度。
4
5- 避免主线程被非UI任务阻塞
6- 减少组件刷新的数量
7- 合理使用缓存提升响应速度
8- 合理使用预加载提升响应速度
9
10## 避免主线程被非UI任务阻塞
11
12在应用响应用户输入期间,应用主线程应尽可能只执行UI任务(待显示数据的准备、可见视图组件的更新等),非UI的耗时任务(长时间加载的内容等)建议通过异步任务延迟处理或者分配到其他线程处理。
13
14### 使用组件异步加载特性
15
16当前系统提供的Image组件默认生效异步加载特性,当应用在页面上展示一批本地图片的时候,会先显示空白占位块,当图片在其他线程加载完毕后,再替换占位块。这样图片加载就可以不阻塞页面的显示,给用户带来良好的交互体验。因此,只在加载图片耗时比较短的情况下建议下述代码。
17
18```typescript
19@Entry
20@Component
21struct ImageExample1 {
22  build() {
23    Column() {
24      Row() {
25        Image('resources/base/media/sss001.jpg')
26          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
27        Image('resources/base/media/sss002.jpg')
28          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
29        Image('resources/base/media/sss003.jpg')
30          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
31        Image('resources/base/media/sss004.jpg')
32          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
33      }
34    // 此处省略若干个Row容器,每个容器内都包含如上的若干Image组件
35    }
36  }
37}
38```
39
40建议:在加载图片的耗时比较短的时候,通过异步加载的效果会大打折扣,建议配置Image的syncLoad属性。
41
42```typescript
43@Entry
44@Component
45struct ImageExample2 {
46  build() {
47    Column() {
48      Row() {
49        Image('resources/base/media/sss001.jpg')
50          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
51        Image('resources/base/media/sss002.jpg')
52          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
53        Image('resources/base/media/sss003.jpg')
54          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
55        Image('resources/base/media/sss004.jpg')
56          .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
57      }
58    // 此处省略若干个Row容器,每个容器内都包含如上的若干Image组件
59    }
60  }
61}
62```
63
64### 使用TaskPool线程池异步处理
65
66当前系统提供了[TaskPool线程池](../reference/apis-arkts/js-apis-taskpool.md),相比worker线程,TaskPool提供了任务优先级设置、线程池自动管理机制,示例如下:
67
68```typescript
69import taskpool from '@ohos.taskpool';
70
71@Concurrent
72function computeTask(arr: string[]): string[] {
73  // 模拟一个计算密集型任务
74  let count = 0;
75  while (count < 100000000) {
76    count++;
77  }
78  return arr.reverse();
79}
80
81@Entry
82@Component
83struct AspectRatioExample3 {
84  @State children: string[] = ['1', '2', '3', '4', '5', '6'];
85
86  aboutToAppear() {
87    this.computeTaskInTaskPool();
88  }
89
90  async computeTaskInTaskPool() {
91    const param = this.children.slice();
92    let task = new taskpool.Task(computeTask, param);
93    await taskpool.execute(task);
94  }
95
96  build() {
97    // 组件布局
98  }
99}
100```
101
102### 创建异步任务
103
104反例:将非UI的耗时任务通过Promise进行Async异步执行。虽然执行了异步操作,但只是将顺序移到了下一个执行的任务位置,还是会影响页面响应速度。
105
106```typescript
107@Entry
108@Component
109struct Index {
110  @State private text: string = "hello world";
111  private count: number = 0;
112
113  aboutToAppear() {
114    const result: Promise<void> = new Promise(() => {
115      this.computeTask();
116    });
117  }
118
119  build() {
120    Column({space: 10}) {
121      Text(this.text).fontSize(50)
122    }
123    .width('100%')
124    .height('100%')
125    .padding(10)
126  }
127
128  computeTask() {
129    this.count = 0;
130    while (this.count < 10000000) {
131      this.count++;
132    }
133  }
134}
135```
136
137建议:将一个长时间执行的非UI任务通过setTimeout改造成异步任务,主线程可以先绘制初始页面。等主线程空闲时,再执行异步任务。
138
139```typescript
140@Entry
141@Component
142struct Index {
143  @State private text: string = "Hello World";
144  private count: number = 0;
145
146  aboutToAppear() {
147    setTimeout(() => {
148      this.computeTask();
149    }, 1000)
150  }
151
152  build() {
153    Column({space: 10}) {
154      Text(this.text).fontSize(50)
155    }
156    .width('100%')
157    .height('100%')
158    .padding(10)
159  }
160
161  computeTask() {
162    this.count = 0;
163    while (this.count < 10000000) {
164      this.count++;
165    }
166  }
167}
168```
169
170通过SmartPerf-Host工具对trace进行抓取,可从应用启动阶段分析响应速度。
171
172
173如下图所示,反例中启动时总耗时为457ms。
174
175![反例总时长](./figures/improve-application-response-promise-all-duration.png)
176
177反例中启动时aboutToAppear阶段耗时为295ms。
178
179![反例时长](./figures/improve-application-response-promise-duration.png)
180
181正例中启动时总耗时为169ms。
182
183![正例总时长](./figures/improve-application-response-settimeout-all-duration.png)
184
185正例中启动时aboutToAppear阶段耗时为167us。
186
187![正例时长](./figures/improve-application-response-settimeout-duration.png)
188
189其中setTimeout真正的耗时为308ms。
190
191![正例时长](./figures/improve-application-response-settimeout-real-duration.png)
192
193异步运行机制如图所示。
194
195![异步运行机制](./figures/improve-application-response-async.png)
196
197## 减少刷新的组件数量
198
199应用刷新页面时需要尽可能减少刷新的组件数量,如果数量过多会导致主线程执行测量、布局的耗时过长,还会在自定义组件新建和销毁过程中,多次调用aboutToAppear()、aboutToDisappear()方法,增加主线程负载。
200
201### 使用指定宽高的容器限制刷新范围
202
203反例:如果一个容器没有同时指定宽高,此时改变容器内部的布局,那么该容器外同级的所有组件都会重新做布局计算和测量更新,导致主线程UI刷新耗时过长。
204
205以下代码的Text('New Page')组件被状态变量isVisible控制,isVisible为true时创建,false时销毁。当isVisible发生变化时,由于其外包裹的Stack容器没有同时指定宽高,
206因此会扩散影响到容器外ForEach中的Text渲染:
207
208```typescript
209const IMAGE_TOTAL_NUM: number = 10; // 图片总数
210
211@Entry
212@Component
213struct StackExample {
214  @State isVisible: boolean = true;
215  private data: number[] = [];
216
217  aboutToAppear() {
218    for (let i: number = 0; i < IMAGE_TOTAL_NUM; i++) {
219      this.data.push(i);
220    }
221  }
222
223  build() {
224    Column() {
225      Button('Switch Hidden and Show').onClick(() => {
226        this.isVisible = !this.isVisible;
227      })
228
229      Stack() {
230        if (this.isVisible) {
231          Text('New Page').width(100).height(30).backgroundColor(0xd2cab3)
232        }
233      }.width(100) // 本案例以Stack容器为例,只指定了宽,会触发父容器组件重新布局计算,引起ForEach中文本测量。
234
235      ForEach(this.data, (item: number) => { // 由于Stack容器没有同时指定宽高,会扩散影响到这一层,引起Text的测量更新。
236        Text(`Item value: ${item}`)
237          .fontSize($r('app.integer.font_size_20'))
238          .width($r('app.string.layout_100_percent'))
239          .textAlign(TextAlign.Center)
240      }, (item: number) => item.toString())
241    }
242  }
243}
244```
245
246
247建议:指定Stack宽高,此时Stack组件作为布局计算的边界,内部的变化不会扩散到父容器,进而减少兄弟节点的刷新。
248
249```typescript
250const IMAGE_TOTAL_NUM: number = 10; // 图片总数
251
252@Entry
253@Component
254struct StackExample2 {
255  @State isVisible: boolean = true;
256  private data: number[] = [];
257
258  aboutToAppear() {
259    for (let i: number = 0; i < IMAGE_TOTAL_NUM; i++) {
260      this.data.push(i);
261    }
262  }
263
264  build() {
265    Column() { // 父容器
266      Button('Switch Hidden and Show').onClick(() => {
267        this.isVisible = !this.isVisible;
268      })
269
270      Stack() {
271        if (this.isVisible) {
272          Text('New Page').width(100).height(30).backgroundColor(0xd2cab3)
273        }
274      }.width(100).height(30) // 在指定宽高的Stack容器内,内部的Text组件变化只会在容器内部做布局和测量更新,不会影响到容器外ForEach中的Text组件。
275
276      ForEach(this.data, (item: number) => { // Stack容器指定了宽高,不会影响到这一层兄弟节点
277        Text(`Item value: ${item}`)
278          .fontSize($r('app.integer.font_size_20'))
279          .width($r('app.string.layout_100_percent'))
280          .textAlign(TextAlign.Center)
281      }, (item: number) => item.toString())
282    }
283  }
284}
285```
286**效果对比**
287
288正反例相同的操作步骤:通过点击按钮,将初始状态为显示的Text('New Page')组件切换为隐藏状态,此时开始抓取耗时,再次点击按钮,将隐藏状态切换为显示状态,此时结束抓取,两次切换间的时间间隔长度,需保证页面渲染完成。
289
290反例:父容器Column内有被只指定了宽的Stack容器包裹的Text组件,其中if条件结果变更会触发创建和销毁该组件,此时会触发父组件兄弟节点重新布局计算,引起ForEach中的文本测量,因此导致主线程UI刷新耗时过长。
291
292当Text('New Page')隐藏状态时开始抓取耗时,此时点击按钮显示Text('New Page')组件时结束抓取,此时引起了兄弟节点中ForEach中的文本测量,Text总共创建个数为stack容器1个Text+兄弟节点中ForEach中的100个Text,共101个,Text总耗时为3ms。
293
294![img](./figures/improve_application_responese_1.png)
295
296基于上例,将Stack容器指定宽高,相同操作抓取耗时,此时没有引起父组件兄弟节点的布局计算和测量更新,仅有Stack容器中的1个Text创建耗时,Text总耗时为255μs。
297
298![img](./figures/improve_application_responses_2.png)
299
300可见,对于可以指定宽高的容器可以限制刷新范围。
301
302### 按需加载列表组件的元素
303
304反例:this.arr中的每一项元素都被初始化和加载,数组中的元素有10000个,主线程执行耗时长。
305
306```typescript
307@Entry
308@Component
309struct MyComponent7 {
310  @State arr: number[] = Array.from(Array<number>(10000), (v,k) =>k);
311  build() {
312    List() {
313      ForEach(this.arr, (item: number) => {
314        ListItem() {
315          Text(`item value: ${item}`)
316        }
317      }, (item: number) => item.toString())
318    }
319  }
320}
321```
322
323建议:这种情况下用LazyForEach替换ForEach,LazyForEach一般只加载可见的元素,避免一次性初始化和加载所有元素。
324
325```typescript
326class BasicDataSource implements IDataSource {
327  private listeners: DataChangeListener[] = []
328
329  public totalCount(): number {
330    return 0
331  }
332
333  public getData(index: number): string {
334    return ''
335  }
336
337  registerDataChangeListener(listener: DataChangeListener): void {
338    if (this.listeners.indexOf(listener) < 0) {
339      console.info('add listener')
340      this.listeners.push(listener)
341    }
342  }
343
344  unregisterDataChangeListener(listener: DataChangeListener): void {
345    const pos = this.listeners.indexOf(listener);
346    if (pos >= 0) {
347      console.info('remove listener')
348      this.listeners.splice(pos, 1)
349    }
350  }
351
352  notifyDataReload(): void {
353    this.listeners.forEach(listener => {
354      listener.onDataReloaded()
355    })
356  }
357
358  notifyDataAdd(index: number): void {
359    this.listeners.forEach(listener => {
360      listener.onDataAdd(index)
361    })
362  }
363
364  notifyDataChange(index: number): void {
365    this.listeners.forEach(listener => {
366      listener.onDataChange(index)
367    })
368  }
369
370  notifyDataDelete(index: number): void {
371    this.listeners.forEach(listener => {
372      listener.onDataDelete(index)
373    })
374  }
375
376  notifyDataMove(from: number, to: number): void {
377    this.listeners.forEach(listener => {
378      listener.onDataMove(from, to)
379    })
380  }
381}
382
383class MyDataSource extends BasicDataSource {
384  private dataArray: string[] = Array.from(Array<number>(10000), (v, k) => k.toString());
385
386  public totalCount(): number {
387    return this.dataArray.length
388  }
389
390  public getData(index: number): string  {
391    return this.dataArray[index]
392  }
393
394  public addData(index: number, data: string): void {
395    this.dataArray.splice(index, 0, data)
396    this.notifyDataAdd(index)
397  }
398
399  public pushData(data: string): void {
400    this.dataArray.push(data)
401    this.notifyDataAdd(this.dataArray.length - 1)
402  }
403}
404
405@Entry
406@Component
407struct MyComponent {
408  private data: MyDataSource = new MyDataSource()
409
410  build() {
411    List() {
412      LazyForEach(this.data, (item: string) => {
413        ListItem() {
414            Text(item).fontSize(20).margin({ left: 10 })
415        }
416      }, (item:string) => item)
417    }
418  }
419}
420```
421## 合理使用缓存提升响应速度
422缓存可以存储经常访问的数据或资源,当下次需要访问相同数据时,可以直接从缓存中获取,避免了重复的计算或请求,从而加快了响应速度。
423### 使用AVPlayer实例缓存提升视频加载速度
424AVPlayer实例的创建与销毁都很消耗性能,针对这个问题可以使用实例缓存进行优化,首次加载页面时创建两个实例,在打开新页面时切换空闲实例,通过reset方法重置实例到初始化状态。优化点在于不需要频繁创建销毁实例,且reset方法性能优于release方法。下面以AVPlayer为例列出正反例对比供参考。
425
426反例:打开新页面时创建实例,离开页面时使用release方法销毁实例。
427```typescript
428import media from '@ohos.multimedia.media';
429
430@Entry
431@Component
432struct Index {
433  private avPlayer: media.AVPlayer | undefined = undefined;
434
435  aboutToAppear(): void {
436    // 页面创建时初始化AVPlayer实例
437    media.createAVPlayer().then((ret) => {
438      this.avPlayer = ret;
439    });
440  }
441
442  aboutToDisappear(): void {
443    // 离开页面时销毁AVPlayer实例
444    if (this.avPlayer) {
445      this.avPlayer.release();
446    }
447    this.avPlayer = undefined;
448  }
449
450  build() {
451    // 组件布局
452  }
453}
454```
455
456正例:首次加载页面时维护两个实例,在切换页面时切换实例,并将之前的实例通过reset方法重置。
457```typescript
458import media from '@ohos.multimedia.media';
459
460@Entry
461@Component
462struct Index {
463  private avPlayer: media.AVPlayer | undefined = undefined;
464  private avPlayerManager: AVPlayerManager = AVPlayerManager.getInstance();
465
466  aboutToAppear(): void {
467    this.avPlayerManager.switchPlayer();
468    this.avPlayer = this.avPlayerManager.getCurrentPlayer();
469  }
470
471  aboutToDisappear(): void {
472    this.avPlayerManager.resetCurrentPlayer();
473    this.avPlayer = undefined;
474  }
475
476  build() {
477    // 组件布局
478  }
479}
480
481class AVPlayerManager {
482  private static instance?: AVPlayerManager;
483
484  private player1?: media.AVPlayer;
485  private player2?: media.AVPlayer;
486  private currentPlayer?: media.AVPlayer;
487
488  public static getInstance(): AVPlayerManager {
489    if (!AVPlayerManager.instance) {
490      AVPlayerManager.instance = new AVPlayerManager();
491    }
492    return AVPlayerManager.instance;
493  }
494
495  async AVPlayerManager() {
496    this.player1 = await media.createAVPlayer();
497    this.player2 = await media.createAVPlayer();
498  }
499
500  /**
501   * 切换页面时切换AVPlayer实例
502   */
503  switchPlayer(): void {
504    if (this.currentPlayer === this.player1) {
505      this.currentPlayer = this.player2;
506    } else {
507      this.currentPlayer = this.player1;
508    }
509  }
510
511  getCurrentPlayer(): media.AVPlayer | undefined {
512    return this.currentPlayer;
513  }
514
515  /**
516   * 使用reset方法重置AVPlayer实例
517   */
518  resetCurrentPlayer(): void {
519    this.currentPlayer?.pause(() => {
520      this.currentPlayer?.reset();
521    });
522  }
523}
524```
525## 合理使用预加载提升响应速度
526
527### 使用NodeContainer提前渲染降低响应时延
528
529应用启动时有广告页的场景下。如果先渲染广告页而后再渲染首页,很可能造成首页响应时延较长,影响用户体验。针对此类问题可以使用NodeContainer在广告页渲染时同步渲染首页,等到跳转到首页时直接送显,提高响应速度。
530
531反例:按次序依次渲染送显
532
533主要代码逻辑如下:
534
5351、模拟广告页,通过点击不同按钮分别进入普通页面和预加载页面
536```typescript
537// Index.ets
538
539import router from '@ohos.router';
540
541@Entry
542@Component
543struct Index {
544  build() {
545    Column({ space: 5 }) {
546      // 进入普通页面
547      Button("普通页面")
548        .type(ButtonType.Capsule)
549        .onClick(() => {
550          router.pushUrl({ url: 'pages/CommonPage' })
551        })
552      // 进入预加载页面
553      Button("预加载页面")
554        .type(ButtonType.Capsule)
555        .onClick(() => {
556          router.pushUrl({ url: 'pages/PreloadedPage' })
557        })
558    }.height('100%')
559    .width('100%')
560    .justifyContent(FlexAlign.Center)
561  }
562}
563```
564
5652、普通首页,也即按顺序普通渲染的页面
566```typescript
567// CommonPage.ets
568
569import { MyBuilder, getNumbers } from '../builder/CustomerBuilder';
570
571@Entry
572@Component
573struct CommonPage {
574  build() {
575    Row() {
576      MyBuilder(getNumbers())
577    }
578  }
579}
580```
5813、自定义builder,用来定制页面结构
582```typescript
583// CustomerBuilder.ets
584
585@Builder
586export function MyBuilder(numbers: string[]) {
587  Column() {
588    List({ space: 20, initialIndex: 0 }) {
589      ForEach(numbers, (item: string) => {
590        ListItem() {
591          Text('' + item)
592            .width('100%')
593            .height(50)
594            .fontSize(16)
595            .textAlign(TextAlign.Center)
596            .borderRadius(10)
597            .backgroundColor(0xFFFFFF)
598        }
599      }, (day: string) => day)
600    }
601    .listDirection(Axis.Vertical) // 排列方向
602    .scrollBar(BarState.Off)
603    .friction(0.6)
604    .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
605    .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
606    .width('90%')
607    .height('100%')
608  }
609  .width('100%')
610  .height('100%')
611  .backgroundColor(0xDCDCDC)
612  .padding({ top: 5 })
613}
614
615export const getNumbers = (): string[] => {
616  const numbers: string[] = [];
617  for (let i = 0; i < 100; i++) {
618    numbers.push('' + i)
619  }
620  return numbers;
621}
622```
623
624正例:在启动时预加载首页
625
626主要代码逻辑如下:
627
6281、应用启动时提前创建首页
629```typescript
630// EntryAbility.ets
631
632import { ControllerManager } from '../builder/CustomerController';
633import { getNumbers } from '../builder/CustomerBuilder';
634
635export default class EntryAbility extends UIAbility {
636  onWindowStageCreate(windowStage: window.WindowStage): void {
637    // Main window is created, set main page for this ability
638    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
639
640    windowStage.loadContent('pages/Index', (err, data) => {
641      if (err.code) {
642        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
643        return;
644      }
645      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
646    });
647    window.getLastWindow(this.context, (err: BusinessError, data) => {
648      if (err.code) {
649        console.error('Failed to obtain top window. Cause:' + JSON.stringify(err));
650        return;
651      }
652      // 提前创建
653      ControllerManager.getInstance().createNode(data.getUIContext(), getNumbers());
654    })
655  }
656
657  onWindowStageDestroy(): void {
658    // Main window is destroyed, release UI related resources
659    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
660    // 清空组件,防止内存泄漏
661    ControllerManager.getInstance().clearNode();
662  }
663}
664```
665
6662、预加载的首页,使用NodeContainer进行占位,当跳转到本页时直接将提前创建完成的首页填充
667```typescript
668// PreloadedPage.ets
669
670import { ControllerManager } from '../builder/CustomerController';
671
672@Entry
673@Component
674struct PreloadedPage {
675  build() {
676    Row() {
677      NodeContainer(ControllerManager.getInstance().getNode())
678    }
679  }
680}
681```
682
6833、自定义NodeController,并提供提前创建首页的能力
684```typescript
685// CustomerController.ets
686
687import { UIContext } from '@ohos.arkui.UIContext';
688import { NodeController, BuilderNode, FrameNode } from "@ohos.arkui.node";
689import { MyBuilder } from './CustomerBuilder';
690
691export class MyNodeController extends NodeController {
692  private rootNode: BuilderNode<[string[]]> | null = null;
693  private wrapBuilder: WrappedBuilder<[string[]]> = wrapBuilder(MyBuilder);
694  private numbers: string[] | null = null;
695
696  constructor(numbers: string[]) {
697    super();
698    this.numbers = numbers;
699  }
700
701  makeNode(uiContext: UIContext): FrameNode | null {
702    if (this.rootNode != null) {
703      // 返回FrameNode节点
704      return this.rootNode.getFrameNode();
705    }
706    // 返回null控制动态组件脱离绑定节点
707    return null;
708  }
709
710  // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
711  initNode(uiContext: UIContext) {
712    if (this.rootNode != null) {
713      return;
714    }
715    // 创建节点,需要uiContext
716    this.rootNode = new BuilderNode(uiContext)
717    // 创建组件
718    this.rootNode.build(this.wrapBuilder, this.numbers)
719  }
720}
721
722export class ControllerManager {
723  private static instance?: ControllerManager;
724  private myNodeController?: MyNodeController;
725
726  static getInstance(): ControllerManager {
727    if (!ControllerManager.instance) {
728      ControllerManager.instance = new ControllerManager();
729    }
730    return ControllerManager.instance;
731  }
732
733  /**
734   * 初始化需要UIContext 需在Ability获取
735   * @param uiContext
736   * @param numbers
737   */
738  createNode(uiContext: UIContext, numbers: string[]) {
739    // 创建NodeController
740    this.myNodeController = new MyNodeController(numbers);
741    this.myNodeController.initNode(uiContext);
742  }
743
744  /**
745   * 自定义获取NodeController实例接口
746   * @returns MyNodeController
747   */
748  getNode(): MyNodeController | undefined {
749    return this.myNodeController;
750  }
751
752  /**
753   * 解除占用,防止内存泄漏
754   */
755  clearNode(): void {
756    this.myNodeController = undefined;
757  }
758}
759```
760
761通过SmartPerf-Host工具抓取相关trace进行分析首页响应时延,其中主要关注两个trace tag分别是DispatchTouchEvent代表点击事件和MarshRSTransactionData代表响应,如下图所示:
762
763反例响应时延:18.1ms
764
765![反例响应时延](./figures/preload_counter_example_delay.png)
766
767正例响应时延:9.4ms
768
769![正例响应时延](./figures/preload_positive_example_delay.png)
770
771由上述对比数据即可得出结论,预加载首页能优化首页响应时延。
772
773### 使用条件渲染实现预加载
774
775当页面较为复杂时,跳转至该页面的响应时长较高。可通过条件渲染的方式,使用**骨架图**作为默认展示,等数据加载完再显示最终布局,从而加快响应速度。
776
777```typescript
778@Entry
779@Component
780struct Index {
781  @State simpleList: Array<number> = [1, 2, 3, 4, 5];
782  @State isInitialized: boolean = false; // 是否已获取数据进行初始化
783  @State isClicked: boolean = false;
784
785  build() {
786    Column() {
787      Button('点击加载')
788        .onClick(() => {
789          this.isClicked = !this.isClicked;
790          setTimeout(() => {
791            this.isInitialized = !this.isInitialized;
792          }, 300);
793        })
794      if (this.isClicked) {
795        ForEach(this.simpleList, (item: number) => {
796          if (!this.isInitialized) {
797            // 未获取数据前使用骨架图
798            ArticleSkeletonView()
799              .margin({ top: 20 })
800          } else {
801            // 获取数据后再刷新显示内容
802            Text('OK')
803            // ...
804          }
805        }, (item: number) => item.toString())
806      }
807    }
808    .padding(20)
809    .width('100%')
810    .height('100%')
811  }
812}
813
814@Builder
815function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
816  Row()
817    .width(width)
818    .height(height)
819    .backgroundColor('#FFF2F3F4')
820}
821
822@Component
823struct ArticleSkeletonView { // 自定义骨架图
824  build() {
825    Row() {
826      Column() {
827        textArea(80, 80)
828      }
829      .margin({ right: 20 })
830
831      Column() {
832        textArea('60%', 20)
833        textArea('50%', 20)
834      }
835      .alignItems(HorizontalAlign.Start)
836      .justifyContent(FlexAlign.SpaceAround)
837      .height('100%')
838    }
839    .padding(20)
840    .borderRadius(12)
841    .backgroundColor('#FFECECEC')
842    .height(120)
843    .width('100%')
844    .justifyContent(FlexAlign.SpaceBetween)
845  }
846}
847```
848
849效果如下图:
850
851![骨架图占位](./figures/improve-application-response-skeleton.png)
852
853将使用和未使用骨架图的组件通过SmartPerf-Host工具抓取trace后对比可得:
854
855未使用骨架图时,响应时间约为321.5ms。(其中包含setTimeout的300ms)
856
857![骨架图占位](./figures/improve-application-response-no-skeleton-duration.png)
858
859使用了骨架图后,响应时间变为10.3ms。
860
861![骨架图占位](./figures/improve-application-response-skeleton-duration.png)
862
863## 延迟执行相机的资源释放操作
864
865将相机的关闭和释放操作放在setTimeout函数中执行,使其延迟到系统相对空闲的时刻进行,可以避免在程序忙碌时段占用关键资源,提升整体性能及响应能力;确保相机资源在系统任务负载减轻时得以释放,维护了应用的稳定性和效率。
866### 反例
867
868这段代码定义了在相机页面隐藏时触发的函数,用于释放相机相关资源。通过“停止拍摄进程 > 暂停并释放相机会话 > 关闭和释放预览及拍照的输入输出对象 > 清空相机管理对象”的过程,确保应用程序在不再使用相机时能够有效管理并回收所有相机资源。但是直接调用的release方法中captureSession、cameraInput、previewOutput、cameraOutput都用了await,使相机关闭和释放顺序执行可能会导致应用程序的响应性下降,造成用户界面卡顿。
869```ts
870// 相机页面每次隐藏时触发一次
871onPageHide() {
872  this.service.release()
873}
874
875// 释放资源
876public async release() {
877  this.stopCapture();
878  if (this.isSessionStart) {
879    try {
880      // 拍照模式会话类暂停
881      await this.captureSession?.stop();
882      // 拍照模式会话类释放
883      await this.captureSession?.release();
884    } catch (e) {
885      logger.error("release session error:",JSON.stringify(e))
886    }
887    this.isSessionStart = false;
888    this.isSessionCapture = false;
889    try {
890      // 拍照输入对象类关闭
891      await this.cameraInput?.close()
892      // 预览输出对象类释放
893      await this.previewOutput?.release()
894      // 拍照输出对象类释放
895      await this.cameraOutput?.release()
896    } catch (e) {
897     logger.error('release input output error:',JSON.stringify(e))
898    }
899    // 相机管理对象置空
900    this.cameraManager = null
901  }
902}
903```
904
905反例trace图
906
907利用Smart-Perf工具分析得到反例trace图,追踪流程从应用侧的`DispatchTouchEvent`(type=1,标识手指离开屏幕)标签开始,到render_service直至RSHardwareThread硬件提交vsync,最终定位到首帧渲染的变化。在直接于`onPageHide`中执行相机关闭与释放操作时,该过程耗时457.5ms。
908
909![](./figures/camera_release.PNG)
910
911### 正例
912
913这个代码片段启动setTimeout异步延迟操作,在200ms后调用release释放关闭相机。其通过“停止拍摄进程 >  并发执行:(暂停并释放相机会话 > 关闭和释放预览及拍照的输入输出对象 > 清空相机管理对象)”的过程,确保应用程序在不再使用相机时能够有效管理并回收所有相机资源。移除await关键字应用于相机资源释放操作,允许异步并发执行,显著减少了主线程阻塞,从而提升了应用性能和响应速度。
914
915```ts
916// 相机页面每次隐藏时触发一次
917onPageHide() {
918  setTimeout(this.service.release, 200)
919}
920
921// 释放资源
922public async release() {
923  // 摄像机在停止拍摄时的生命周期
924  this.stopCapture();
925  if (this.isSessionStart) {
926    try {
927      // 拍照模式会话类暂停
928      await this.captureSession?.stop();
929      // 拍照模式会话类释放
930      this.captureSession?.release();
931    } catch (e) {
932      logger.error("release session error:",JSON.stringify(e))
933    }
934    this.isSessionStart = false;
935    this.isSessionCapture = false;
936    try {
937      // 拍照输入对象类关闭
938      await this.cameraInput?.close()
939      // 预览输出对象类释放
940      this.previewOutput?.release()
941      // 拍照输出对象类释放
942      this.cameraOutput?.release()
943    } catch (e) {
944     logger.error('release input output error:',JSON.stringify(e))
945    }
946    // 相机管理对象置空
947    this.cameraManager = null
948  }
949}
950```
951
952正例trace图
953
954而利用Smart-Perf工具分析得到正例trace图,追踪流程从应用侧的`DispatchTouchEvent`(type=1,标识手指离开屏幕)标签开始,到render_service直至RSHardwareThread硬件提交vsync,最终定位到首帧渲染的变化。而通过在`onPageHide`中引入200ms的`setTimeout`延迟机制,执行时间减少至85.6ms。
955
956![](./figures/camera_release_use_settimeout.PNG)
957
958### 性能比对
959
960(注:不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考。)
961
962| 操作类型        | 执行时间    | 备注                                            |
963| ----------- | ------- | --------------------------------------------- |
964| 直接关闭与释放(反例) | 457.5ms | 在`onPageHide`中直接执行相机关闭与释放操作                   |
965| 延时关闭与释放(正例) | 85.6ms  | 在`onPageHide`中使用`setTimeout`延迟200ms后执行关闭与释放操作 |
966
967正反例数据表明,合理运用延时策略能显著提升函数执行效率,是优化相机资源管理与关闭操作性能的有效手段,对提升整体用户体验具有重要价值。
968
969## 减小拖动识别距离
970
971应用识别拖动手势事件时需要设置合理的拖动距离,设置不合理的拖动距离会导致滑动不跟手、响应时延慢等问题。针对此类问题可以通过设置distance大小来解决。
972
973### 反例
974
975指定触发拖动手势事件的最小拖动距离为100vp
976
977```ts
978import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'
979
980@Entry
981@Component
982struct PanGestureExample {
983  @State offsetX: number = 0
984  @State offsetY: number = 0
985  @State positionX: number = 0
986  @State positionY: number = 0
987  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right })
988
989  build() {
990    Column() {
991      Column() {
992        Text('PanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
993      }
994      .height(200)
995      .width(300)
996      .padding(20)
997      .border({ width: 3 })
998      .margin(50)
999      .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) // 以组件左上角为坐标原点进行移动
1000      // 左右拖动触发该手势事件
1001      .gesture(
1002        PanGesture(this.panOption)
1003          .onActionStart((event: GestureEvent) => {
1004            console.info('Pan start')
1005            hiTraceMeter.startTrace("PanGesture", 1)
1006          })
1007          .onActionUpdate((event: GestureEvent) => {
1008            if (event) {
1009              this.offsetX = this.positionX + event.offsetX
1010              this.offsetY = this.positionY + event.offsetY
1011            }
1012          })
1013          .onActionEnd(() => {
1014            this.positionX = this.offsetX
1015            this.positionY = this.offsetY
1016            console.info('Pan end')
1017            hiTraceMeter.finishTrace("PanGesture", 1)
1018          })
1019      )
1020
1021      Button('修改PanGesture触发条件')
1022        .onClick(() => {
1023          this.panOption.setDistance(100)
1024        })
1025    }
1026  }
1027}
1028```
1029
1030利用Profiler工具分析得到反例trace图,其中主要关注两个trace tag分别是DispatchTouchEvent代表点击事件和PanGesture代表响应,追踪流程从应用侧的DispatchTouchEvent(type=0,标识手指接触屏幕)标签开始,到PanGesture(事件响应)的变化,该过程耗时145.1ms。
1031
1032反例trace图
1033
1034![反例响应时延](./figures/pangesture_distance_max.png)
1035
1036日志主要关注从应用接收TouchDown事件到pan识别耗时,该过程耗时127ms。(注:日志信息和trace图非同一时间获取,所获得的性能数据存在差异,提供的数值仅供参考。)
1037
1038反例日志
1039
1040![反例响应时延日志](./figures/pangesture_distance_max_log.png)
1041
1042### 正例
1043
1044指定触发拖动手势事件的最小拖动距离为4vp
1045
1046```ts
1047import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'
1048
1049@Entry
1050@Component
1051struct PanGestureExample {
1052  @State offsetX: number = 0
1053  @State offsetY: number = 0
1054  @State positionX: number = 0
1055  @State positionY: number = 0
1056  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right })
1057
1058  build() {
1059    Column() {
1060      Column() {
1061        Text('PanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
1062      }
1063      .height(200)
1064      .width(300)
1065      .padding(20)
1066      .border({ width: 3 })
1067      .margin(50)
1068      .translate({ x: this.offsetX, y: this.offsetY, z: 0 }) // 以组件左上角为坐标原点进行移动
1069      // 左右拖动触发该手势事件
1070      .gesture(
1071        PanGesture(this.panOption)
1072          .onActionStart((event: GestureEvent) => {
1073            console.info('Pan start')
1074            hiTraceMeter.startTrace("PanGesture", 1)
1075          })
1076          .onActionUpdate((event: GestureEvent) => {
1077            if (event) {
1078              this.offsetX = this.positionX + event.offsetX
1079              this.offsetY = this.positionY + event.offsetY
1080            }
1081          })
1082          .onActionEnd(() => {
1083            this.positionX = this.offsetX
1084            this.positionY = this.offsetY
1085            console.info('Pan end')
1086            hiTraceMeter.finishTrace("PanGesture", 1)
1087          })
1088      )
1089
1090      Button('修改PanGesture触发条件')
1091        .onClick(() => {
1092          this.panOption.setDistance(4)
1093        })
1094    }
1095  }
1096}
1097```
1098
1099利用Profiler工具分析得到正例trace图,其中主要关注两个trace tag分别是DispatchTouchEvent代表点击事件和PanGesture代表响应,追踪流程从应用侧的DispatchTouchEvent(type=0,标识手指接触屏幕)标签开始,到PanGesture(事件响应)的变化,该过程耗时38.4ms。
1100
1101正例trace图
1102
1103![正例响应时延](./figures/pangesture_distance_min.png)
1104
1105日志主要关注从应用接收TouchDown事件到pan识别耗时,该过程耗时42ms。(注:日志信息和trace图非同一时间获取,所获得的性能数据存在差异,提供的数值仅供参考。)
1106
1107正例日志
1108
1109![正例响应时延日志](./figures/pangesture_distance_min_log.png)
1110
1111### 性能比对
1112(注:不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考,该表格仅分析trace图。)
1113
1114| 拖动距离设置              | 执行时间 | 备注                                                         |
1115| ------------------------- | -------- | ------------------------------------------------------------ |
1116| 最小拖动距离100vp(反例) | 145.1ms  | 最小拖动距离过大会导致滑动脱手、响应时延慢等问题导致性能劣化 |
1117| 最小拖动距离4vp(正例)   | 38.4ms   | 设置合理的拖动距离优化性能                                     |
1118
1119正反例数据表明,合理减小拖动距离能显著提升执行效率,是优化响应时延的有效手段,对提升整体用户体验具有重要价值。(注:本案例通过设置较大和较小拖动距离进行数据对比得出相关结论。distance的默认值为5vp,设置过小的distance容易出现误触等问题,开发者可根据具体应用场景进行设置。)
1120