1# 减少首帧绘制时的冗余操作 2 3------ 4 5## 应用冷启动与加载绘制首页 6 7应用冷启动即当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用。 8 9应用冷启动过程大致可分成以下四个阶段:应用进程创建&初始化、Application&Ability初始化、Ability生命周期、**加载绘制首页**。 10 11 12 13**加载绘制首页**不仅是应用冷启动的四个阶段之一,还是首帧绘制最重要的阶段。而它可以分为三个阶段:加载页面、测量和布局、渲染。本文从这三个阶段入手,分成下面三个场景进行案例优化。 14 15 16 17## 减少加载页面时间 18 19减少加载页面时间可以通过按需加载、减少自定义组件生命周期耗时两种方法来实现。 20 21#### 按需加载 22 23按需加载可以避免一次性初始化和加载所有元素,从而使首帧绘制时加载页面阶段的创建列表元素时间大大减少,从而提升性能表现。 24 25**案例:每一个列表元素都被初始化和加载,为了突出效果,方便观察,设定数组中的元素有10000个,使其在加载页面阶段创建列表内元素耗时大大增加。** 26 27```ts 28@Entry 29@Component 30struct AllLoad { 31 @State arr: String[] = Array.from(Array<string>(10000), (val,i) =>i.toString()); 32 build() { 33 List() { 34 ForEach(this.arr, (item: string) => { 35 ListItem() { 36 Text(`item value: ${item}`) 37 .fontSize(20) 38 .margin({ left: 10 }) 39 } 40 }, (item: string) => item.toString()) 41 } 42 } 43} 44``` 45 46**优化:LazyForEach替换ForEach,避免一次性初始化和加载所有元素。** 47 48```ts 49class BasicDataSource implements IDataSource { 50 private listeners: DataChangeListener[] = []; 51 private originDataArray: string[] = []; 52 53 public totalCount(): number { 54 return 0; 55 } 56 57 public getData(index: number): string { 58 return this.originDataArray[index] 59 } 60 61 // 注册数据改变的监听器 62 registerDataChangeListener(listener: DataChangeListener): void { 63 if (this.listeners.indexOf(listener) < 0) { 64 console.info('add listener') 65 this.listeners.push(listener) 66 } 67 } 68 69 // 注销数据改变的监听器 70 unregisterDataChangeListener(listener: DataChangeListener): void { 71 const pos = this.listeners.indexOf(listener); 72 if (pos >= 0) { 73 console.info('remove listener') 74 this.listeners.splice(pos, 1) 75 } 76 } 77 78 // 通知组件重新加载所有数据 79 notifyDataReload(): void { 80 this.listeners.forEach(listener => { 81 listener.onDataReloaded() 82 }) 83 } 84 85 // 通知组件index的位置有数据添加 86 notifyDataAdd(index: number): void { 87 this.listeners.forEach(listener => { 88 listener.onDataAdd(index) 89 }) 90 } 91 92 // 通知组件index的位置有数据有变化 93 notifyDataChange(index: number): void { 94 this.listeners.forEach(listener => { 95 listener.onDataChange(index) 96 }) 97 } 98 99 // 通知组件删除index位置的数据并刷新LazyForEach的展示内容 100 notifyDataDelete(index: number): void { 101 this.listeners.forEach(listener => { 102 listener.onDataDelete(index) 103 }) 104 } 105 106 // 通知组件数据有移动 107 notifyDataMove(from: number, to: number): void { 108 this.listeners.forEach(listener => { 109 listener.onDataMove(from, to) 110 }) 111 } 112} 113 114class MyDataSource extends BasicDataSource { 115 private dataArray: string[] = Array.from(Array<string>(10000), (val, i) => i.toString()); 116 117 public totalCount(): number { 118 return this.dataArray.length 119 } 120 121 public getData(index: number): string { 122 return this.dataArray[index] 123 } 124 125 public addData(index: number, data: string): void { 126 this.dataArray.splice(index, 0, data) 127 this.notifyDataAdd(index) 128 } 129 130 public pushData(data: string): void { 131 this.dataArray.push(data) 132 this.notifyDataAdd(this.dataArray.length - 1) 133 } 134} 135 136@Entry 137@Component 138struct SmartLoad { 139 private data: MyDataSource = new MyDataSource() 140 141 build() { 142 List() { 143 LazyForEach(this.data, (item: string) => { 144 ListItem() { 145 Text(`item value: ${item}`) 146 .fontSize(20) 147 .margin({ left: 10 }) 148 } 149 }, (item:string) => item) 150 } 151 } 152} 153``` 154 155 156 157#### 减少自定义组件生命周期时间 158 159LoadPage阶段需要等待自定义组件生命周期aboutToAppear的高耗时任务完成, 导致LoadPage时间大量增加,阻塞主线程后续的布局渲染,所以自定义组件生命周期的耗时任务应当转为Worker线程任务,优先绘制页面,避免启动时阻塞在startWindowIcon页面。 160 161**案例:自定义组件生命周期存在高耗时任务,阻塞主线程布局渲染。** 162 163```ts 164@Entry 165@Component 166struct TaskSync { 167 @State private text: string = ""; 168 private count: number = 0; 169 170 aboutToAppear() { 171 this.text = 'hello world'; 172 this.computeTask(); // 同步任务 173 } 174 175 build() { 176 Column({space: 10}) { 177 Text(this.text).fontSize(50) 178 } 179 .width('100%') 180 .height('100%') 181 .padding(10) 182 } 183 184 computeTask() { 185 this.count = 0; 186 while (this.count < 100000000) { 187 this.count++; 188 } 189 this.text = 'task complete'; 190 } 191} 192``` 193 194**优化:自定义组件生命周期的耗时任务转为Worker线程任务,优先绘制页面,再将Worker子线程结果发送到主线程并更新到页面。** 195 196```ts 197// TaskAsync.ets 198import worker from '@ohos.worker'; 199 200@Entry 201@Component 202struct TaskAsync { 203 @State private text: string = ""; 204 private workerInstance:worker.ThreadWorker = new worker.ThreadWorker("entry/ets/workers/worker.ts"); 205 206 aboutToAppear() { 207 // 处理来自子线程的消息 208 this.workerInstance.onmessage = (message)=> { 209 console.info('message from worker: ' + JSON.stringify(message)) 210 this.text = JSON.parse(JSON.stringify(message)).data 211 this.workerInstance.terminate() 212 } 213 this.text = 'hello world'; 214 // 执行Worker线程任务 215 this.computeTaskAsync(); 216 } 217 218 build() { 219 Column({space: 10}) { 220 Text(this.text).fontSize(50) 221 } 222 .width('100%') 223 .height('100%') 224 .padding(10) 225 } 226 private async computeTaskAsync(){ 227 // 发送消息到子线程 228 this.workerInstance.postMessage('hello world') 229 } 230} 231``` 232 233```ts 234// worker.ts 235import worker from '@ohos.worker'; 236 237let parentPort = worker.workerPort; 238 239function computeTask(count: number) { 240 while (count < 100000000) { 241 count++; 242 } 243 return 'task complete' 244} 245// 处理来自主线程的消息 246parentPort.onmessage = (message) => { 247 console.info("onmessage: " + JSON.stringify(message)); 248 // 发送消息到主线程 249 parentPort.postMessage(computeTask(0)); 250} 251``` 252 253 254 255## 减少布局时间 256 257减少布局时间可以通过异步加载和减少视图嵌套层次两种方法来实现。 258 259#### 异步加载 260 261同步加载的操作,使创建图像任务需要在主线程完成,页面布局Layout需要等待创建图像makePixelMap任务的执行,导致布局时间延长。相反,异步加载的操作,在其他线程完成,和页面布局Layout同时开始,且没有阻碍页面布局,所以页面布局更快,性能更好。但是,并不是所有的加载都必须使用异步加载,建议加载尺寸较小的本地图片时将syncLoad设为true,因为耗时较短,在主线程上执行即可。 262 263**案例:使用Image组件同步加载高分辨率图片,阻塞UI线程,增加了页面布局总时间。** 264 265```ts 266@Entry 267@Component 268struct SyncLoadImage { 269 @State arr: String[] = Array.from(Array<string>(100), (val,i) =>i.toString()); 270 build() { 271 Column() { 272 Row() { 273 List() { 274 ForEach(this.arr, (item: string) => { 275 ListItem() { 276 Image($r('app.media.4k')) 277 .border({ width: 1 }) 278 .borderStyle(BorderStyle.Dashed) 279 .height(100) 280 .width(100) 281 .syncLoad(true) 282 } 283 }, (item: string) => item.toString()) 284 } 285 } 286 } 287 } 288} 289``` 290 291**优化:使用Image组件默认的异步加载方式加载图片,不阻塞UI线程,降低页面布局时间。** 292 293```ts 294@Entry 295@Component 296struct AsyncLoadImage { 297 @State arr: String[] = Array.from(Array<string>(100), (val,i) =>i.toString()); 298 build() { 299 Column() { 300 Row() { 301 List() { 302 ForEach(this.arr, (item: string) => { 303 ListItem() { 304 Image($r('app.media.4k')) 305 .border({ width: 1 }) 306 .borderStyle(BorderStyle.Dashed) 307 .height(100) 308 .width(100) 309 } 310 }, (item: string) => item.toString()) 311 } 312 } 313 } 314 } 315} 316``` 317 318 319 320#### 减少视图嵌套层次 321 322视图的嵌套层次会影响应用的性能。通过减少不合理的容器组件,可以使布局深度降低,布局时间减少,优化布局性能,提升用户体验。 323 324**案例:通过Grid网格容器一次性加载1000个网格,并且额外使用3层Flex容器模拟不合理的深嵌套场景使布局时间增加。** 325 326```ts 327@Entry 328@Component 329struct Depth1 { 330 @State number: Number[] = Array.from(Array<number>(1000), (val, i) => i); 331 scroller: Scroller = new Scroller() 332 333 build() { 334 Column() { 335 Grid(this.scroller) { 336 ForEach(this.number, (item: number) => { 337 GridItem() { 338 Flex() { 339 Flex() { 340 Flex() { 341 Text(item.toString()) 342 .fontSize(16) 343 .backgroundColor(0xF9CF93) 344 .width('100%') 345 .height(80) 346 .textAlign(TextAlign.Center) 347 .border({width:1}) 348 } 349 } 350 } 351 } 352 }, (item:string) => item) 353 } 354 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 355 .columnsGap(0) 356 .rowsGap(0) 357 .size({ width: "100%", height: "100%" }) 358 } 359 } 360} 361``` 362 363**优化:通过Grid网格容器一次性加载1000个网格,去除额外的不合理的布局容器,降低布局时间。** 364 365```ts 366@Entry 367@Component 368struct Depth2 { 369 @State number: Number[] = Array.from(Array<number>(1000), (val, i) => i); 370 scroller: Scroller = new Scroller() 371 372 build() { 373 Column() { 374 Grid(this.scroller) { 375 ForEach(this.number, (item: number) => { 376 GridItem() { 377 Text(item.toString()) 378 .fontSize(16) 379 .backgroundColor(0xF9CF93) 380 .width('100%') 381 .height(80) 382 .textAlign(TextAlign.Center) 383 .border({width:1}) 384 } 385 }, (item:string) => item) 386 } 387 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 388 .columnsGap(0) 389 .rowsGap(0) 390 .size({ width: "100%", height: "100%" }) 391 } 392 } 393} 394``` 395 396 397 398## 减少渲染时间 399 400减少渲染时间可以通过条件渲染替代显隐控制的方法来实现。 401 402#### 条件渲染 403 404通过条件渲染替代显隐控制,首帧绘制时的渲染时间明显降低,从而提升性能表现。另外,即使组件处于隐藏状态,在页面刷新时仍存在重新创建过程,因此当对性能有严格要求时建议使用条件渲染代替。 405 406**案例:通过visibility属性控制当前组件显示或隐藏。** 407 408```ts 409@Entry 410@Component 411struct VisibilityExample { 412 build() { 413 Column() { 414 Column() { 415 // 隐藏不参与占位 416 Text('None').fontSize(9).width('90%').fontColor(0xCCCCCC) 417 Row().visibility(Visibility.None).width('90%').height(80).backgroundColor(0xAFEEEE) 418 419 // 隐藏参与占位 420 Text('Hidden').fontSize(9).width('90%').fontColor(0xCCCCCC) 421 Row().visibility(Visibility.Hidden).width('90%').height(80).backgroundColor(0xAFEEEE) 422 423 // 正常显示,组件默认的显示模式 424 Text('Visible').fontSize(9).width('90%').fontColor(0xCCCCCC) 425 Row().visibility(Visibility.Visible).width('90%').height(80).backgroundColor(0xAFEEEE) 426 }.width('90%').border({ width: 1 }) 427 }.width('100%').margin({ top: 5 }) 428 } 429} 430``` 431 432**优化:通过条件渲染替代显隐控制。** 433 434```ts 435@Entry 436@Component 437struct IsVisibleExample { 438 @State isVisible : boolean = true; 439 440 build() { 441 Column(){ 442 Column() { 443 //不渲染即达到隐藏不参与占位 444 Text('None').fontSize(9).width('90%').fontColor(0xCCCCCC) 445 if (!this.isVisible) { 446 Row().width('90%').height(80).backgroundColor(0xAFEEEE) 447 } 448 449 // 隐藏参与占位无法通过条件渲染实现 450 451 // 渲染即正常占位显示 452 Text('Visible').fontSize(9).width('90%').fontColor(0xCCCCCC) 453 if (this.isVisible){ 454 Row().width('90%').height(80).backgroundColor(0xAFEEEE) 455 } 456 }.width('90%').border({ width: 1 }) 457 }.width('100%').margin({ top: 5 }) 458 } 459} 460``` 461