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 176 177反例中启动时aboutToAppear阶段耗时为295ms。 178 179 180 181正例中启动时总耗时为169ms。 182 183 184 185正例中启动时aboutToAppear阶段耗时为167us。 186 187 188 189其中setTimeout真正的耗时为308ms。 190 191 192 193异步运行机制如图所示。 194 195 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 295 296基于上例,将Stack容器指定宽高,相同操作抓取耗时,此时没有引起父组件兄弟节点的布局计算和测量更新,仅有Stack容器中的1个Text创建耗时,Text总耗时为255μs。 297 298 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 766 767正例响应时延:9.4ms 768 769 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 852 853将使用和未使用骨架图的组件通过SmartPerf-Host工具抓取trace后对比可得: 854 855未使用骨架图时,响应时间约为321.5ms。(其中包含setTimeout的300ms) 856 857 858 859使用了骨架图后,响应时间变为10.3ms。 860 861 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 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 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 1035 1036日志主要关注从应用接收TouchDown事件到pan识别耗时,该过程耗时127ms。(注:日志信息和trace图非同一时间获取,所获得的性能数据存在差异,提供的数值仅供参考。) 1037 1038反例日志 1039 1040 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 1104 1105日志主要关注从应用接收TouchDown事件到pan识别耗时,该过程耗时42ms。(注:日志信息和trace图非同一时间获取,所获得的性能数据存在差异,提供的数值仅供参考。) 1106 1107正例日志 1108 1109 1110 1111### 性能比对 1112(注:不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考,该表格仅分析trace图。) 1113 1114| 拖动距离设置 | 执行时间 | 备注 | 1115| ------------------------- | -------- | ------------------------------------------------------------ | 1116| 最小拖动距离100vp(反例) | 145.1ms | 最小拖动距离过大会导致滑动脱手、响应时延慢等问题导致性能劣化 | 1117| 最小拖动距离4vp(正例) | 38.4ms | 设置合理的拖动距离优化性能 | 1118 1119正反例数据表明,合理减小拖动距离能显著提升执行效率,是优化响应时延的有效手段,对提升整体用户体验具有重要价值。(注:本案例通过设置较大和较小拖动距离进行数据对比得出相关结论。distance的默认值为5vp,设置过小的distance容易出现误触等问题,开发者可根据具体应用场景进行设置。) 1120