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 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 122 123【优化后】不存在嵌套export *,从目标文件中直接import。 124 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 171 172【优化后】使用import { One }按需引用。 173 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.ets和SubPage.ets**,而首页直接import { MainPage } from 'library/Index'的方式会导致应用在冷启动过程中执行了非冷启动强相关文件**SubPage.ets**,增加了冷启动耗时。 231 232【优化前】加载模块时执行了非冷启动相关文件SubPage.ets。 233 234 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 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 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 419 420【优化方案一】拆分HAR导出文件。 421 422 423【优化方案二】导入冷启动文件时全路径展开。 424 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 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 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 507 508【优化后】使用HAR代替HSP。 509 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 745 746将网络请求提前至AbilityStage/UIAbility生命的onCreate()生命周期回调函数中,可以将首刷或二刷的时间提前,减少用户等待时间。此处为了体现性能收益,将网络请求放到了更早的AbilityStage的onCreate()生命周期回调中。 747 748【优化后】网络请求提前至AbilityStage的onCreate()周期回调中。 749 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 930 931【优化后】优化网络请求时机后。 932 933 934对比数据如下: 935 936| 方案 | 阶段时长(毫秒) | 937|-----------|:----------:| 938| 优化网络请求时机前 | 1700 | 939| 优化网络请求时机后 | 885.3 | 940 941因此,可以通过提前网络请求的方式减少应用冷启动耗时。 942 943### 使用本地缓存首页数据 944 945使用本地缓存首页数据是优化应用性能的关键一环。它能有效缩短冷启动时的白屏或白块时间,显著提升用户体验。该策略通过预先存储并优先展示缓存中的首页数据,减少了对外部资源(如网络)的依赖,从而加快数据加载速度。当数据更新时,应用则智能地从网络等渠道获取最新内容,确保信息的时效性与准确性。使用本地缓存首页数据,不仅让应用响应更迅速,还显著优化了整体运行流畅度,为用户带来更加顺畅的体验。更多实现细节与性能提升分析,请参见[合理使用缓存提升性能](./reasonable_using_cache_improve_performance.md)。