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