• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 提升应用冷启动速度
2
3<!--Kit: Ability Kit-->
4<!--Subsystem: Ability-->
5<!--Owner: @mgy917-->
6<!--Designer: @jiangwensai-->
7<!--Tester: @Lyuxin-->
8<!--Adviser: @huipeizi-->
9
10应用启动时延是影响用户体验的关键要素。当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这个启动方式就叫做冷启动。
11
12## 分析应用冷启动耗时
13
14应用冷启动过程大致可分成以下五个阶段:应用进程创建&初始化、Application&Ability初始化、Ability/AbilityStage生命周期、加载绘制首页、网络数据二次刷新,如下图:
15
16![](figures/application-cold-start.png)
17
181. **应用进程创建&初始化阶段**:该阶段主要是系统完成应用进程的创建以及初始化的过程,包含了启动页图标(startWindowIcon)的解码。
192. **Application&Ability初始化**:该阶段主要是资源加载、虚拟机创建、Application&Ability相关对象的创建与初始化、依赖模块的加载等。
203. **Ability/AbilityStage生命周期**:该阶段主要是AbilityStage/Ability的启动生命周期,执行相应的生命周期回调。
214. **加载绘制首页**:该阶段主要是加载首页内容、测量布局、刷新组件并绘制。
225. **网络数据二次刷新**:该阶段主要是应用根据业务需要对网络数据进行请求、处理、二次刷新。
23
24可见如果想要提升应用冷启动速度,需要缩短以上几个阶段的耗时。
25
26>**说明:**
27>
28> 1. 关于本文中示例,可参考:[提升应用冷启动速度示例](https://gitcode.com/openharmony/applications_app_samples/tree/master/code/DocsSample/Ability/Performance/Startup)29> 2. 如何使用SmartPerf工具分析冷启动可参考:[应用冷启动分析](performance-optimization-using-smartperf-host.md#应用冷启动分析)。
30
31
32## 1、缩短应用进程创建&初始化阶段耗时
33
34该阶段主要是系统完成应用进程的创建以及初始化的过程,包含了启动页图标(startWindowIcon)的解码。
35
36### 设置合适分辨率的startWindowIcon
37
38该优化场景仅支持rk3568开发板。如果启动页图标分辨率过大,解码耗时会影响应用的启动速度,建议启动页图标分辨率不超过256像素*256像素,如下所示:
39
40```json
41    "abilities": [
42      {
43        "name": "EntryAbility",
44        "srcEntry": "./ets/entryability/EntryAbility.ets",
45        "description": "$string:EntryAbility_desc",
46        "icon": "$media:icon",
47        "label": "$string:EntryAbility_label",
48        "startWindowIcon": "$media:startIcon", // 在这里修改启动页图标,建议不要超过256像素x256像素
49        "startWindowBackground": "$color:start_window_background",
50        "exported": true,
51        "skills": [
52          {
53            "entities": [
54              "entity.system.home"
55            ],
56            "actions": [
57              "action.system.home"
58            ]
59          }
60        ]
61      }
62    ]
63```
64
65下面使用[SmartPerf](https://gitee.com/openharmony/developtools_smartperf_host)工具,对使用优化前的启动页图标(4096像素\*4096像素)及使用优化后的启动页图标(144像素\*144像素)的启动性能进行对比分析。分析阶段的起点为点击应用图标打开应用时触发的触摸事件(即`ProcessTouchEvent`的结束点),阶段终点为应用第一次接到vsync(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
66
67对比数据如下(性能耗时数据因设备版本而异,以实测为准):
68
69| 方案          | 阶段时长(毫秒) |
70|-------------|:--------:|
71| 使用优化前的启动页图标 |  998.6   |
72| 使用优化后的启动页图标 |  722.5   |
73
74可见阶段时长已缩短,故设置合适分辨率的startWindowIcon对缩短应用进程创建&初始化阶段耗时是有效的。
75
76## 2、缩短Application&Ability初始化阶段耗时
77
78该阶段主要是资源加载、虚拟机创建、Application&Ability相关对象的创建与初始化、依赖模块的加载等。
79主要耗时点在于资源加载阶段,分为主要的三个步骤:文件加载、依赖模块解析、文件执行。
801. 文件加载:查找并解析所有的文件到模块中记录。
812. 依赖模块解析(实例化):分配内存空间来存放模块所有导出的变量,但这时候内存中并没有分配变量的值。
823. 文件执行:运行ets文件,将内存中之前未分配值的变量赋为真实的值。
83
84下面将针对这三个阶段可能存在的优化手段进行详细展开说明。
85
86### 减少使用嵌套export *的方式全量导出
87应用冷启动过程中,会在**HandleLaunchAbility**中执行冷启动相关.ets文件,所有被主页面import的.ets文件均会被执行,包括数据结构、变量、全局函数的初始化等。首页需要用到的变量及函数等可能来源于其他ets文件,通过export的形式提供给首页使用。
88例:Numbers文件导出`export One`,需要在MainPage.ets中使用,尽量直接导入或者只嵌套一层Index文件,即在MainPage.ets中直接`import { One } from './Numbers'`。避免在Utils文件`export * from './Numbers'`,在SecondPage文件再次`export * from './Utils'`,最后在A文件中`import * from './SecondPage'`。
89以下为示例代码:
90【优化前】存在多层嵌套export *的方式全量导出。
91```ts
92// Numbers.ets
93export const One: number = 1;
94
95// ...
96// 此处嵌套多层export *
97
98// Utils.ets
99export * from './Numbers';
100
101// SecondPage.ets
102export * from './Utils';
103
104// Index.ets
105import * from './SecondPage';
106```
107【优化后】不存在嵌套export *,从目标文件中直接import。
108```ts
109// 去掉冗余嵌套的export,即在Index.ets中直接import { One } from './Numbers'。
110
111// Numbers.ets
112export const  One: number = 1;
113
114// Index.ets
115import { One } from './Numbers';
116```
117由于依赖模块解析采用深度优先遍历的方式来遍历模块依赖关系图中每一个模块记录,会先从入口文件的第一个导入语句开始一层层往更深层查找,直到最后一个没有导入语句的模块为止,连接好这个模块的导出变量之后会回到上一级的模块继续这个步骤,因此多层export *的使用会导致依赖模块解析、文件执行阶段耗时增长。
118针对上述示例代码关注该阶段耗时差异,对优化前后启动性能进行对比分析。分析阶段的起点为开始加载abc文件(即`H:JSPandaFileExecutor::ExecuteFromAbcFile`),阶段终点为`abc文件`加载完成。
119
120【优化前】存在8层嵌套export *。
121![](./figures/application_coldstart1.png)
122
123【优化后】不存在嵌套export *,从目标文件中直接import。
124![](./figures/application_coldstart2.png)
125
126对比数据如下:
127
128| 方案          |  阶段时长(微秒)  |
129|-------------|:----------:|
130| (优化前)存在8层嵌套export * |   492.6    |
131| (优化后)不存在嵌套export *,从目标文件中直接import |   388.7    |
132
133可见阶段时长已缩短。因此减少多层文件的嵌套导出export *可以提升应用冷启动速度。
134
135>**说明:**
136>
137>本示例中嵌套层次较浅,从时间上观测到的收益不明显,当实际开发过程中可能会涉及到更加复杂的情况,修改后对性能收益会更明显。
138
139### 减少import *的方式全量引用
140应用程序加载过程中,需要使用不同模块中的变量或函数,通常应用开发者会将相同类型的变量或函数放在同一个工具类文件当中,使用时通过import的方式引入对应的模块,当工具类中存在较多暴露函数或变量时,推荐直接import对应的变量,可以减少该阶段中.ets文件执行耗时,即减少文件中所有export变量的初始化过程。
141以下为示例代码:
142【优化前】Index.ets中使用 import * as nm from '../utils/Numbers'。
143```ts
144// Index.ets
145import * as nm from '../utils/Numbers'; // 不推荐import *的方式
146hilog.info(0x0000, 'testTag', '%{public}d', nm.One); // 此处仅用到变量One
147
148// Numbers.ets
149export const One: number = 1;
150export const Two: number = 2;
151// ...
152// 此处省略2000条数据
153```
154
155【优化后】Index.ets中使用 import { One } from '../utils/Numbers'。
156```ts
157// Index.ets
158import { One } as nm from '../utils/Numbers'; // 推荐按需引用变量
159hilog.info(0x0000, 'testTag', '%{public}d', One); // 此处仅用到变量One
160
161// Numbers.ets
162export const One: number = 1;
163export const Two: number = 2;
164// ...
165// 此处省略2000条数据
166```
167下面对优化前后启动性能进行对比分析,分析阶段的起点为UI Ability Launching开始(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility(const std::shared_ptr<AbilityLocalRecord> &`)的开始点),阶段终点为UI Ability Launching结束(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility(const std::shared_ptr<AbilityLocalRecord> &)`的结束点)。
168
169【优化前】使用import * as nm全量引用2000条数据。
170![](./figures/application_coldstart3.png)
171
172【优化后】使用import { One }按需引用。
173![](./figures/application_coldstart4.png)
174
175对比数据如下:
176
177| 方案          |   阶段时长(毫秒)    |
178|-------------|:-------------:|
179| (优化前)使用import * as nm全量引用 |     16.7      |
180| (优化后)使用import { One }按需引用 |      7.1      |
181
182可见阶段时长已缩短。因此将非UI耗时操作移至子线程中处理,可以缩短应用冷启动完成时延。
183
184>**说明:**
185>
186> **此优化方案仅可将冷启动阶段耗时缩短,但是可能导致其他场景耗时增长,即变量初始化过程从冷启动阶段分摊至其他使用阶段**。
187> 例:二级页面使用到Number中Two变量,此方案会使二级页面跳转过程对比优化前耗时更长。
188
189### 减少使用未引用的import模块
190
191应用代码执行前,应用程序必须找到并加载import的所有模块。应用程序启动时会因加载并使用的每个额外第三方框架或模块而增加启动耗时,耗时长短取决于加载的第三方框架或者模块的数量和大小。推荐开发者尽可能使用系统提供的模块,按需加载,来缩短应用程序的启动耗时。
192
193以下为示例代码:
194
195```ts
196// 优化减少import的模块
197// import { particleAbility, featureAbility, wantConstant } from '@kit.AbilityKit';
198// import { FormExtensionAbility } from '@kit.FormKit';
199// import { GesturePath, GesturePoint } from '@kit.AccessibilityKit';
200// import { distributedAccount, BusinessError, osAccount } from '@kit.BasicServicesKit';
201// import { webview } from '@kit.ArkWeb';
202
203import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
204import { window } from '@kit.ArkUI';
205import { hilog } from '@kit.PerformanceAnalysisKit';
206
207export default class EntryAbility extends UIAbility {
208  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
209    // ...
210    // webview.WebviewController.initializeWebEngine();
211  }
212  // ...
213}
214```
215
216需要说明,这里为了方便对比该阶段导入模块和不导入模块的性能差异,用一个@ohos.web.webview模块进行对比,这里假设@ohos.web.webview模块并非该阶段必须导入的模块。而在实际业务场景中,由于webview初始化时间较长,如果放到页面跳转时再导入该模块进行使用,有可能会劣化页面跳转的完成时延。因此,模块是否能在该阶段进行优化还需要结合具体业务场景进行评估。
217
218下面使用DevEco Studio内置的Profiler中的启动分析工具Launch,对优化import的模块前(导入@ohos.web.webview模块)及优化import的模块后(不导入@ohos.web.webview模块)的启动性能进行对比分析。下面是Launch工具抓取的UI Ability Launching(加载UI Ability)耗时,对比数据如下(性能耗时数据因设备型号版本而异,以实测为准):
219
220| 方案           | UI Ability Launching阶段时长(毫秒) |
221|--------------|:----------------------------:|
222| 优化import的模块前 |              26              |
223| 优化import的模块后 |              10              |
224
225可见阶段时长已缩短,故减少import的模块对缩短Application&Ability初始化阶段耗时是有效的。这里建议该阶段应减少应用启动时非必要的import模块,按需加载模块,缩短应用程序的启动耗时。
226
227### 合理拆分导出文件,减少冗余文件执行
228应用程序加载模块后,需要执行应用侧的.ets文件,对其进行初始化,并执行全局初始化变量、函数。可以将文件分为两类,一类为冷启动强相关文件(如首页展示界面及组件相关文件),一类为非冷启动强相关文件(如跳转后二级页面),在冷启动过程中仅执行冷启动强相关文件,来缩短应用的启动耗时。
229**场景示例:**
230应用存在两个页面,首页Index展示为HAR包中**MainPage.ets**的Text组件,该文件中不包含耗时操作;首页点击Text跳转至**SecondPage**,其中引用了HAR包中的**SubPage.ets**,该文件存在全局函数的耗时操作,会在模块加载时执行。但是HAR包中的导出文件Index.ets同时导出了**MainPage.etsSubPage.ets**,而首页直接import { MainPage } from 'library/Index'的方式会导致应用在冷启动过程中执行了非冷启动强相关文件**SubPage.ets**,增加了冷启动耗时。
231
232【优化前】加载模块时执行了非冷启动相关文件SubPage.ets233
234![](./figures/application_coldstart5.png)
235
236以下为示例代码:
237```ts
238// entry/src/main/ets/pages/Index.ets
239import { MainPage } from 'library/Index'; // 不推荐用法:直接导入了与冷启动非强相关文件SubPage.ets
240export struct Index{
241  @Provide pathStack: NavPathStack = new NavPathStack();
242  build() {
243    Navigation(this.pathStack) {
244      Row() {
245        // 引用HAR的自定义组件
246        MainPage()
247      }
248    }
249  }
250}
251
252// library/src/main/ets/components/mainpage/MainPage.ets
253@Component
254export struct MainPage {
255  @Consume pathStack: NavPathStack;
256  @State message: string = 'HAR MainPage';
257  build() {
258    Row() {
259      Text(this.message)
260        .fontSize(32)
261        .fontWeight(FontWeight.Bold)
262    }.onClick(() => {
263      this.pathStack.pushPath({ name: 'SecondPage' });
264    })
265  }
266}
267
268// entry/src/main/ets/pages/SecondPage.ets
269import { SubPage } from 'library/Index';
270@Builder
271export function SecondPageBuilder() {
272  SecondPage()
273}
274@Entry
275@Component
276struct SecondPage {
277  pathStack: NavPathStack = new NavPathStack();
278  build() {
279    NavDestination() {
280      Row() {
281        // 引用HAR的自定义组件
282        SubPage()
283      }
284      .height('100%')
285    }
286    .onReady((context: NavDestinationContext) => {
287      this.pathStack = context.pathStack;
288    })
289  }
290}
291
292// library/src/main/ets/components/mainpage/SubPage.ets
293// SubPage中的全局耗时函数
294const LARGE_NUMBER = 10000000;
295function computeTask(): number {
296  let count = 0;
297  while (count < LARGE_NUMBER) {
298    count++;
299  }
300  return count;
301}
302let count = computeTask();
303// ...
304
305// library/Index.ets
306export { MainPage } from './src/main/ets/components/mainpage/MainPage'; // 冷启动强相关文件
307export { SubPage } from './src/main/ets/components/mainpage/SubPage'; // 非冷启动强相关文件
308```
309
310**【优化方案一】**
311将HAR包的导出文件**Index.ets**进行拆分,**IndexAppStart.ets**文件仅导出首页相关文件,即**MainPage.ets**。**IndexOthers.ets**文件导出非首页相关文件,即**SubPage.ets。**
312**优点**:使用此种方案优化后可以将冷启阶段(加载首页文件)与非冷启阶段(加载非首页文件)需要执行的.ets文件进行完全拆分,类比其他需优化的场景也可以使用本方案进行拆分。
313**缺点**:需保证拆分后IndexAppStart.ets中的导出文件不存在对于IndexOthers.ets中的导出文件的引用。
314
315【图一】拆分HAR导出文件
316
317![](./figures/application_coldstart6.png)
318
319以下为示例代码:
3201. 将HAR包的导出文件Index.ets进行拆分,IndexAppStart.ets文件仅导出首页相关文件,IndexOthers.ets文件导出非首页相关文件。
321```ts
322// library/IndexAppStart.ets
323export { MainPage } from './src/main/ets/components/mainpage/MainPage';
324// library/IndexOthers.ets
325export { SubPage } from './src/main/ets/components/mainpage/SubPage';
326```
3272. 首页Index从IndexAppStart.ets导入MainPage。
328```ts
329// Index.ets
330import { MainPage } from 'library/IndexAppStart';
331
332@Entry
333@Component
334struct Index {
335  @Provide pathStack: NavPathStack = new NavPathStack();
336
337  build() {
338    Navigation(this.pathStack) {
339      Row() {
340        // 引用HAR的自定义组件
341        MainPage()
342      }
343    }
344    .height('100%')
345    .width('100%')
346  }
347}
348```
3493. 跳转后的页面SecondPage从IndexOthers.ets导入SubPage。
350```ts
351// SecondPage.ets
352import { SubPage } from 'library/IndexOthers';
353
354@Builder
355export function SecondPageBuilder() {
356  SecondPage()
357}
358
359@Entry
360@Component
361struct SecondPage {
362  pathStack: NavPathStack = new NavPathStack();
363
364  build() {
365    NavDestination() {
366      Row() {
367        // 引用HAR的自定义组件
368        SubPage()
369      }
370      .height('100%')
371    }
372    .onReady((context: NavDestinationContext) => {
373      this.pathStack = context.pathStack;
374    })
375  }
376}
377```
378**【优化方案二】**
379在首页的**Index.ets**文件中导入**MainPage.ets**时使用全路径展开。
380**优点**:不需要新增文件来汇总导出所有冷启阶段文件。
381**缺点**:引用时需要对所有冷启阶段文件进行路径展开,增加开发和维护成本。
382
383【图二】首页导入冷启动文件时使用全路径展开
384
385![](./figures/application_coldstart7.png)
386
387以下为示例代码:
388```ts
389// Index.ets
390import { MainPage } from 'library/src/main/ets/components/mainpage/MainPage';
391
392@Entry
393@Component
394struct Index {
395  @Provide pathStack: NavPathStack = new NavPathStack();
396
397  build() {
398    Navigation(this.pathStack) {
399      Row() {
400        // 引用HAR的自定义组件
401        MainPage()
402      }
403    }
404    .height('100%')
405    .width('100%')
406  }
407}
408```
409>**说明:**
410>
411>1. **上述两种优化方案默认MainPage中不存在对于SubPage中的import。**
412>2. **当存在MainPage对于SubPage的直接import时,需要使用[动态import](../arkts-utils/arkts-dynamic-import.md)方法来进行优化。**
413>3. 开发者可自行根据优化方案的优缺点权衡选择合适的优化方案。
414
415下面对优化前后启动性能进行对比分析。阶段起点为`UI Ability Launching`的开始点,阶段终点为应用首帧即`First Frame - App Phase`的开始点。
416
417【优化前】加载模块时执行了非冷启动相关文件。
418![](./figures/application_coldstart8.png)
419
420【优化方案一】拆分HAR导出文件。
421![](./figures/application_coldstart9.png)
422
423【优化方案二】导入冷启动文件时全路径展开。
424![](./figures/application_coldstart10.png)
425
426优化前后的对比数据如下:
427
428| 方案        |  阶段时长(毫秒)  |
429|-----------|:----------:|
430| 优化前 |   140.1    |
431| 优化方案一(拆分HAR导出文件) |    62.9    |
432| 优化方案二(导入冷启动文件时全路径展开) |    61.3    |
433
434可见阶段时长已缩短,因此可以通过拆分HAR包导出的Index.ets文件或导入冷启动文件时路径全展开的方案,减少应用冷启动中.ets文件执行耗时,从而提升应用冷启动速度。
435
436### 使用延迟加载Lazy-Import减少冷启动冗余文件执行
437
438可以通过延迟加载 [lazy-import](../arkts-utils/arkts-lazy-import.md) 延缓对冷启动时暂不执行的冗余文件的加载,而在后续导出变量被真正使用时再同步加载执行文件,节省资源以提高应用冷启动性能。
439详细使用指导请参考[延迟加载lazy-import使用指导](Lazy-Import-Instructions.md)。
440
441### 减少多个HSP/HAP对于相同HAR的引用
442
443在应用开发的过程中,可以使用[HSP](../quick-start/in-app-hsp.md)或[HAR](../quick-start/har-package.md)的共享包方式将同类的模块进行整合,用于实现多个模块或多个工程间共享ArkUI组件、资源等相关代码。
444由于共享包的动态和静态差异,在多HAP/HSP引用相同HAR包的情况下,会存在HAR包中的单例失效,从而影响到应用冷启动的性能。
445
446【优化前】HAP包和HSP包分别引用相同HAR包。
447
448![](./figures/application_coldstart11.png)
449
450如上图所示,工程内存在三个模块,HAP包为应用主入口模块,HSP为应用主界面显示模块,HAR_COMMON集成了所有通用工具类,其中funcResult为func方法的执行结果。
451由于HAP和HSP模块同时引用HAR_COMMON模块时会破坏HAR的单例模式,所以HAP和HSP模块使用**HAR_COMMON**中的**funcResult**时,会导致func方法在两个模块加载时各执行一次,使得文件执行时间耗时增长。
452如果仅从性能的角度考虑,可以使用以下方式进行修改,从而达到缩短冷启动阶段耗时的目的。
453
454【优化后】切换为HAP包和HAR包分别引用相同HAR包。
455
456![](./figures/application_coldstart12.png)
457
458>**说明:**
459>
460> 1. 在多HAP/HSP引用相同HAR包的情况下,若HSP包和HAR包均能满足业务需求,建议将HSP包改成HAR包。
461> 2. 若使用的HSP为集成态HSP,可跳过该优化方案。
462
463以下为示例代码:
4641. 在被引用HAR_COMMON包中写入功能示例。
465```ts
466// har_common/src/main/ets/utils/Utils.ets
467const LARGE_NUMBER = 100000000;
468function func(): number {
469  let count = 0;
470  while (count < LARGE_NUMBER) {
471    count++;
472  }
473  return count;
474}
475export let funcResult = func();
476```
4772. 分别通过使用HSP包和HAR包来引用该HAR_COMMON包中的功能进行性能对比实验。
478- 使用HAP包和HSP包引用该HAR_COMMON包中的功能。
479  HAP包引用HAR_COMMON包中的功能。
480
481  ```ts
482  // entry/src/main/ets/pages/Index.ets
483  import { MainPage } from 'hsp_library';
484  import { funcResult } from 'har_common';
485  ```
486  HSP包引用HAR_COMMON包中的功能。
487  ```ts
488  // hsp_library/src/main/ets/pages/MainPage.ets
489  import { funcResult } from 'har_common';
490  ```
491- 使用HAP包和HAR包引用该HAR_COMMON包中的功能。
492  HAP包引用HAR_COMMON包中的功能。
493  ```ts
494  // entry/src/main/ets/pages/Index.ets
495  import { MainPage } from 'har_library';
496  import { funcResult } from 'har_common';
497  ```
498  HAR包引用HAR_COMMON包中的功能。
499  ```ts
500  // har_library/src/main/ets/pages/MainPage.ets
501  import { funcResult } from 'har_common';
502  ```
503  下面对优化前后启动性能进行对比分析。分析阶段的起点为启动Ability(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility`的开始点),阶段终点为应用第一次接到vsync(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
504
505【优化前】使用HSP包。
506![](./figures/application_coldstart13.png)
507
508【优化后】使用HAR代替HSP。
509![](./figures/application_coldstart14.png)
510
511优化前后的对比数据如下:
512
513| 方案               |  阶段时长(毫秒)  |
514|------------------|:----------:|
515| (优化前)使用HSP包      |    3125    |
516| (优化后)使用HAR代替HSP |   853.9    |
517
518从测试数据可以看出,将HSP替换为HAR包后,应用启动的阶段耗时明显缩短。
519
520>**说明:**
521>
522> 上述示例为凸显出差异,func执行函数循环次数为100000000,开发者实际修改后收益需根据实际情况测试。
523
524
525## 3、缩短AbilityStage生命周期阶段耗时
526
527该阶段主要是AbilityStage的启动生命周期,执行相应的生命周期回调。
528
529### 避免在AbilityStage生命周期回调接口进行耗时操作
530
531在应用启动流程中,系统会执行AbilityStage的生命周期回调函数。因此,不建议在这些回调函数中执行耗时过长的操作,耗时操作建议通过异步任务延迟处理或者放到其他线程执行。
532
533在这些生命周期回调里,推荐开发者只做必要的操作,详情可以参考:[AbilityStage组件管理器](../application-models/abilitystage.md)。
534
535以下为示例代码:
536
537```ts
538const LARGE_NUMBER = 10000000;
539const DELAYED_TIME = 1000;
540
541export default class MyAbilityStage extends AbilityStage {
542  onCreate(): void {
543    // 耗时操作
544    // this.computeTask();
545    this.computeTaskAsync(); // 异步任务
546  }
547
548  onAcceptWant(want: Want): string {
549    // 仅specified模式下触发
550    return 'MyAbilityStage';
551  }
552
553  computeTask(): void {
554    let count = 0;
555    while (count < LARGE_NUMBER) {
556      count++;
557    }
558  }
559
560  private computeTaskAsync(): void {
561    setTimeout(() => { // 这里使用setTimeout来实现异步延迟运行
562      this.computeTask();
563    }, DELAYED_TIME);
564  }
565}
566```
567
568下面使用[SmartPerf](https://gitee.com/openharmony/developtools_smartperf_host)工具,对优化前同步执行耗时操作及优化后异步执行耗时操作的启动性能进行对比分析。分析阶段的起点为启动Ability(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility`的开始点),阶段终点为应用第一次接到vsync(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
569
570对比数据如下:
571
572|                        | 阶段开始(秒)   | 阶段结束(秒)   | 阶段时长(秒) |
573| ---------------------- | -------------- | -------------- | ------------ |
574| 优化前同步执行耗时操作 | 2124.915558194 | 2127.041354575 | 2.125796381  |
575| 优化后异步执行耗时操作 | 4186.436835246 | 4186.908777335 | 0.471942089  |
576
577可见阶段时长已缩短,故避免在AbilityStage生命周期回调接口进行耗时操作对缩短AbilityStage生命周期阶段耗时是有效的。
578
579## 4、缩短Ability生命周期阶段耗时
580
581该阶段主要是Ability的启动生命周期,执行相应的生命周期回调。
582
583### 非UI耗时操作并行化
584在应用启动流程中,主要聚焦在执行UI相关操作中,为了更快的能显示首页内容,不建议在主线程中执行非UI相关的耗时操作,耗时操作建议通过异步任务进行延迟处理或放到其他子线程中执行,线程并发方案可以参考:[TaskPool和Worker的对比实践](../arkts-utils/multi-thread-concurrency-overview.md)。
585在冷启动过程中如果存在图片下载、网络请求前置数据、数据反序列化等非UI操作可以根据开发者实际情况移至子线程中进行,参考下面文章:[避免在主线程中执行耗时操作](avoid_time_consuming_operations_in_mainthread.md)。
586
587### 避免在Ability生命周期回调接口进行耗时操作
588
589在应用启动流程中,系统会执行Ability的生命周期回调函数。因此,不建议在这些回调函数中执行耗时过长的操作,耗时操作建议通过异步任务延迟处理或者放到其他线程执行。
590
591在这些生命周期回调里,推荐开发者只做必要的操作,下面以UIAbility为例进行说明。关于UIAbility组件生命周期的详细说明,参见[UIAbility组件生命周期](../application-models/uiability-lifecycle.md)。
592
593```ts
594const LARGE_NUMBER = 10000000;
595const DELAYED_TIME = 1000;
596
597export default class EntryAbility extends UIAbility {
598  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
599    hilog.info(0x0000, 'testTag', 'Ability onCreate');
600    // 耗时操作
601    // this.computeTask();
602    this.computeTaskAsync(); // 异步任务
603  }
604
605  onDestroy(): void {
606    hilog.info(0x0000, 'testTag', 'Ability onDestroy');
607  }
608
609  onWindowStageCreate(windowStage: window.WindowStage): void {
610    hilog.info(0x0000, 'testTag', 'Ability onWindowStageCreate');
611
612    windowStage.loadContent('pages/Index', (err, data) => {
613      if (err.code) {
614        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: ' + JSON.stringify(err) ?? '');
615        return;
616      }
617      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: ' + JSON.stringify(data) ?? '');
618    });
619
620    // 耗时操作
621    // this.computeTask();
622    this.computeTaskAsync(); // 异步任务
623  }
624
625  onWindowStageDestroy(): void {
626    hilog.info(0x0000, 'testTag', 'Ability onWindowStageDestroy');
627  }
628
629  onForeground(): void {
630    hilog.info(0x0000, 'testTag', 'Ability onForeground');
631    // 耗时操作
632    // this.computeTask();
633    this.computeTaskAsync(); // 异步任务
634  }
635
636  onBackground(): void {
637    hilog.info(0x0000, 'testTag', 'Ability onBackground');
638  }
639
640  computeTask(): void {
641    let count = 0;
642    while (count < LARGE_NUMBER) {
643      count++;
644    }
645  }
646
647  private computeTaskAsync(): void {
648    setTimeout(() => { // 这里使用setTimeout来实现异步延迟运行
649      this.computeTask();
650    }, DELAYED_TIME);
651  }
652}
653```
654
655下面使用[SmartPerf](https://gitee.com/openharmony/developtools_smartperf_host)工具,对优化前同步执行耗时操作及优化后异步执行耗时操作的启动性能进行对比分析。分析阶段的起点为启动Ability(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility`的开始点),阶段终点为应用第一次接到vsync(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
656
657对比数据如下:
658
659| 方案          | 阶段开始(秒)   | 阶段结束(秒)   | 阶段时长(秒) |
660|-------------| -------------- | -------------- | ------------ |
661| 优化前同步执行耗时操作 | 1954.987630036 | 1957.565964504 | 2.578334468  |
662| 优化后异步执行耗时操作 | 4186.436835246 | 4186.908777335 | 0.471942089  |
663
664可见阶段时长已缩短,故避免在Ability生命周期回调接口进行耗时操作对缩短Ability生命周期阶段耗时是有效的。
665
666## 5、缩短加载绘制首页阶段耗时
667
668该阶段主要是加载首页内容、测量布局、刷新组件并绘制。
669
670### 自定义组件生命周期回调接口里避免耗时操作
671
672自定义组件的生命周期变更会调用相应的回调函数。
673
674aboutToAppear函数会在创建自定义组件实例后,页面绘制之前执行,以下代码在aboutToAppear中对耗时间的计算任务进行了异步处理,避免在该接口执行该耗时操作,不阻塞页面绘制。
675
676以下为示例代码:
677
678```ts
679const LARGE_NUMBER = 10000000;
680const DELAYED_TIME = 1000;
681
682@Entry
683@Component
684struct Index {
685  @State private text: string | undefined = "";
686  private count: number = 0;
687
688  aboutToAppear() {
689    // 耗时操作
690    // this.computeTask();
691    this.computeTaskAsync(); // 异步任务
692    let context = this.getUIContext().getHostContext();
693    this.text = context?.resourceManager.getStringSync($r('app.string.startup_text').id);
694  }
695
696  build() {
697    Column({ space: 10 }) {
698      Text(this.text).fontSize(50)
699    }
700    .width('100%')
701    .height('100%')
702    .padding(10)
703  }
704
705  computeTask(): void {
706    this.count = 0;
707    while (this.count < LARGE_NUMBER) {
708      this.count++;
709    }
710    let context = this.getUIContext().getHostContext();
711    this.text = context?.resourceManager.getStringSync($r('app.string.task_text').id);
712  }
713
714  // 运算任务异步处理
715  private computeTaskAsync(): void {
716    setTimeout(() => { // 这里使用setTimeout来实现异步延迟运行
717      this.computeTask();
718    }, DELAYED_TIME);
719  }
720}
721```
722
723下面使用[SmartPerf](https://gitee.com/openharmony/developtools_smartperf_host)工具,对优化前同步执行耗时操作及优化后异步执行耗时操作的启动性能进行对比分析。分析阶段的起点为启动Ability(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility`的开始点),阶段终点为应用第一次接到vsync(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
724
725对比数据如下:
726
727| 方案          | 阶段开始(秒)   | 阶段结束(秒)   | 阶段时长(秒) |
728|-------------| -------------- | -------------- | ------------ |
729| 优化前同步执行耗时操作 | 3426.272974492 | 3431.785898837 | 5.512924345  |
730| 优化后异步执行耗时操作 | 4186.436835246 | 4186.908777335 | 0.471942089  |
731
732可见阶段时长已缩短,因此在自定义组件生命周期回调接口中避免耗时操作可以缩短加载绘制首页阶段耗时。
733
734## 6、缩短网络数据二次刷新阶段耗时
735
736该阶段主要是应用根据业务需要对网络数据进行请求、处理、二次刷新。
737
738### 网络请求提前发送
739当前大多数应用的首页内容需从网络获取,发送网络请求的时机显得尤为重要。应用发送网络请求后等待网络数据的返回,网络请求的这段时间应用可以继续执行启动流程,直到网络数据返回后进行解析,反序列化之后就可以加载首页数据,因此网络请求的发起时机越早,整个冷启动的完成时延阶段越短。
740
741可将网络请求及网络请求前的初始化流程放置在AbilityStage/UIAbility的onCreate()生命周期中,在AbilityStage/UIAbility中仅执行网络相关预处理,等待网络请求发送后可继续执行首页数据准备、UI相关操作。在服务端处理流程相同的情况下,应用可以更早的拿到网络数据并行展示。
742
743【优化前】应用首页框架加载时进行网络数据请求。
744![](./figures/application_coldstart15.png)
745
746将网络请求提前至AbilityStage/UIAbility生命的onCreate()生命周期回调函数中,可以将首刷或二刷的时间提前,减少用户等待时间。此处为了体现性能收益,将网络请求放到了更早的AbilityStage的onCreate()生命周期回调中。
747
748【优化后】网络请求提前至AbilityStage的onCreate()周期回调中。
749![](./figures/application_coldstart16.png)
750
751以下为示例代码:
752【优化前】:在首页根组件的onAppear()生命周期回调中发起网络请求。
753```ts
754// entry/src/main/ets/pages/Index.ets
755import { httpRequest } from '../utils/NetRequest';
756import { number } from '../utils/Calculator';
757
758AppStorage.link('netData');
759PersistentStorage.persistProp('netData', undefined);
760
761@Entry
762@Component
763struct Index {
764  @State message: string = 'Hello World' + number; // 为了体现性能收益,引用耗时函数的执行结果number
765  @StorageLink('netData') netData: PixelMap | undefined = undefined;
766  build(){
767    Row(){
768      Image(this.netData)
769        .objectFit(ImageFit.Contain)
770        .width('50%')
771        .height('50%')
772    }
773    .onAppear(() => {
774      // 发送网络请求
775      httpRequest();
776    })
777  }
778}
779
780// entry/src/main/ets/utils/NetRequest.ets
781import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
782import { http } from '@kit.NetworkKit';
783import { BusinessError } from '@kit.BasicServicesKit';
784import { image } from '@kit.ImageKit';
785// 通过http的request方法从网络下载图片资源
786export function httpRequest() {
787  hiTraceMeter.startTrace('Http Request', 1);
788  http.createHttp()
789    // 实际开发需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址
790    .request('https://www.example1.com/POST?e=f&g=h',
791      (error: BusinessError, data: http.HttpResponse) => {
792        if (error) {
793          // 下载失败时不执行后续逻辑
794          return;
795        }
796        // 处理网络请求返回的数据
797        transcodePixelMap(data);
798      }
799    )
800}
801// 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
802function transcodePixelMap(data: http.HttpResponse) {
803  if (http.ResponseCode.OK === data.responseCode) {
804    const imageData: ArrayBuffer = data.result as ArrayBuffer;
805    // 通过ArrayBuffer创建图片源实例
806    const imageSource: image.ImageSource = image.createImageSource(imageData);
807    const options: image.InitializationOptions = {
808      'alphaType': 0, // 透明度
809      'editable': false, // 是否可编辑
810      'pixelFormat': 3, // 像素格式
811      'scaleMode': 1, // 缩略值
812      'size': { height: 100, width: 100 }
813    }; // 创建图片大小
814    // 通过属性创建PixelMap
815    imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
816      AppStorage.set('netData', pixelMap);
817      hiTraceMeter.finishTrace('Http Request', 1);
818    });
819  }
820}
821
822// entry/src/main/ets/utils/Calculator.ets
823const LARGE_NUMBER = 100000000;
824function computeTask(): number {
825  let count = 0;
826  while (count < LARGE_NUMBER) {
827    count++;
828  }
829  return count;
830}
831export let number = computeTask();
832```
833
834【优化后】
8351. 在NetRequest.ets中进行Http请求以及数据处理。
836```ts
837// NetRequest.ets
838import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
839import { http } from '@kit.NetworkKit';
840import { BusinessError } from '@kit.BasicServicesKit';
841import { image } from '@kit.ImageKit';
842// 通过http的request方法从网络下载图片资源
843export function httpRequest() {
844  hiTraceMeter.startTrace('Http Request', 1);
845  http.createHttp()
846    // 实际开发需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址
847    .request('https://www.example1.com/POST?e=f&g=h',
848      (error: BusinessError, data: http.HttpResponse) => {
849        if (error) {
850          // 下载失败时不执行后续逻辑
851          return;
852        }
853        // 处理网络请求返回的数据
854        transcodePixelMap(data);
855      }
856    )
857}
858
859// 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
860function transcodePixelMap(data: http.HttpResponse) {
861  if (http.ResponseCode.OK === data.responseCode) {
862    const imageData: ArrayBuffer = data.result as ArrayBuffer;
863    // 通过ArrayBuffer创建图片源实例
864    const imageSource: image.ImageSource = image.createImageSource(imageData);
865    const options: image.InitializationOptions = {
866      'alphaType': 0, // 透明度
867      'editable': false, // 是否可编辑
868      'pixelFormat': 3, // 像素格式
869      'scaleMode': 1, // 缩略值
870      'size': { height: 100, width: 100 }
871    }; // 创建图片大小
872    // 通过属性创建PixelMap
873    imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
874      AppStorage.set('netData', pixelMap);
875      hiTraceMeter.finishTrace('Http Request', 1);
876    });
877  }
878}
879```
8802. 在AbilityStage的onCreate()生命周期回调中发起网络请求。
881```ts
882// MyAbilityStage.ets
883import { AbilityStage, Want } from '@kit.AbilityKit';
884import { httpRequest } from '../utils/NetRequest';
885export default class MyAbilityStage extends AbilityStage {
886  onCreate(): void {
887    // 发送网络请求
888    httpRequest();
889  }
890
891  onAcceptWant(want: Want): string {
892    // 仅specified模式下触发
893    return 'MyAbilityStage';
894  }
895}
896```
8973. 在首页Index.ets中展示请求获取的图片。
898```ts
899// Index.ets
900import { number } from '../utils/Calculator';
901
902AppStorage.link('netData');
903PersistentStorage.persistProp('netData', undefined);
904
905@Entry
906@Component
907struct Index {
908  @State message: string = 'Hello World' + number; // 为了体现性能收益,引用耗时函数的执行结果number
909  @StorageLink('netData') netData: PixelMap | undefined = undefined;
910  build() {
911    Row() {
912      Image(this.netData)
913        .objectFit(ImageFit.Contain)
914        .width('50%')
915        .height('50%')
916    }
917    .onDisAppear(() => {
918      AppStorage.set('netData', undefined);
919    })
920    .height('100%')
921    .width('100%')
922  }
923}
924```
925
926下面对优化前后启动性能进行对比分析,分析阶段的起点为启动Ability(即`H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility`的开始点),阶段终点为应用接收到网络数据返回后的首帧刷新(即`H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int`的开始点)。
927
928【优化前】优化网络请求时机前。
929![](./figures/application_coldstart17.png)
930
931【优化后】优化网络请求时机后。
932![](./figures/application_coldstart18.png)
933
934对比数据如下:
935
936| 方案        |  阶段时长(毫秒)  |
937|-----------|:----------:|
938| 优化网络请求时机前 |    1700    |
939| 优化网络请求时机后 |   885.3    |
940
941因此,可以通过提前网络请求的方式减少应用冷启动耗时。
942
943### 使用本地缓存首页数据
944
945使用本地缓存首页数据是优化应用性能的关键一环。它能有效缩短冷启动时的白屏或白块时间,显著提升用户体验。该策略通过预先存储并优先展示缓存中的首页数据,减少了对外部资源(如网络)的依赖,从而加快数据加载速度。当数据更新时,应用则智能地从网络等渠道获取最新内容,确保信息的时效性与准确性。使用本地缓存首页数据,不仅让应用响应更迅速,还显著优化了整体运行流畅度,为用户带来更加顺畅的体验。更多实现细节与性能提升分析,请参见[合理使用缓存提升性能](./reasonable_using_cache_improve_performance.md)。