1# 状态管理优秀实践 2 3 4为了帮助应用程序开发人员提高其应用程序质量,特别是在高效的状态管理方面。本章节面向开发者提供了多个在开发ArkUI应用中常见的低效开发的场景,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如何正确地在应用开发中使用状态变量,进行高性能开发。 5 6## 使用@ObjectLink代替@Prop减少不必要的深拷贝 7 8在应用开发中,开发者经常会进行父子组件的数值传递,而在不会改变子组件内状态变量值的情况下,使用@Prop装饰状态变量会导致组件创建的耗时增加,从而影响一部分性能。 9 10【反例】 11 12```ts 13@Observed 14class MyClass { 15 public num: number = 0; 16 17 constructor(num: number) { 18 this.num = num; 19 } 20} 21 22@Component 23struct PropChild { 24 @Prop testClass: MyClass; // @Prop 装饰状态变量会深拷贝 25 26 build() { 27 Text(`PropChild testNum ${this.testClass.num}`) 28 } 29} 30 31@Entry 32@Component 33struct Parent { 34 @State testClass: MyClass[] = [new MyClass(1)]; 35 36 build() { 37 Column() { 38 Text(`Parent testNum ${this.testClass[0].num}`) 39 .onClick(() => { 40 this.testClass[0].num += 1; 41 }) 42 43 // PropChild没有改变@Prop testClass: MyClass的值,所以这时最优的选择是使用@ObjectLink 44 PropChild({ testClass: this.testClass[0] }) 45 } 46 } 47} 48``` 49 50在上文的示例中,PropChild组件没有改变\@Prop testClass: MyClass的值,所以这时较优的选择是使用\@ObjectLink,因为\@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候\@ObjectLink是比\@Link和\@Prop更优的选择。 51 52【正例】 53 54```ts 55@Observed 56class MyClass { 57 public num: number = 0; 58 59 constructor(num: number) { 60 this.num = num; 61 } 62} 63 64@Component 65struct PropChild { 66 @ObjectLink testClass: MyClass; // @ObjectLink 装饰状态变量不会深拷贝 67 68 build() { 69 Text(`PropChild testNum ${this.testClass.num}`) 70 } 71} 72 73@Entry 74@Component 75struct Parent { 76 @State testClass: MyClass[] = [new MyClass(1)]; 77 78 build() { 79 Column() { 80 Text(`Parent testNum ${this.testClass[0].num}`) 81 .onClick(() => { 82 this.testClass[0].num += 1; 83 }) 84 85 // 当子组件不需要发生本地改变时,优先使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择 86 PropChild({ testClass: this.testClass[0] }) 87 } 88 } 89} 90``` 91 92 93## 不使用状态变量强行更新非状态变量关联组件 94 95【反例】 96 97 98```ts 99@Entry 100@Component 101struct MyComponent { 102 @State needsUpdate: boolean = true; 103 realStateArr: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器 104 realState: Color = Color.Yellow; 105 106 updateUIArr(param: Array<number>): Array<number> { 107 const triggerAGet = this.needsUpdate; 108 return param; 109 } 110 updateUI(param: Color): Color { 111 const triggerAGet = this.needsUpdate; 112 return param; 113 } 114 build() { 115 Column({ space: 20 }) { 116 ForEach(this.updateUIArr(this.realStateArr), 117 (item: Array<number>) => { 118 Text(`${item}`) 119 }) 120 Text("add item") 121 .onClick(() => { 122 // 改变realStateArr不会触发UI视图更新 123 this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1); 124 125 // 触发UI视图更新 126 this.needsUpdate = !this.needsUpdate; 127 }) 128 Text("chg color") 129 .onClick(() => { 130 // 改变realState不会触发UI视图更新 131 this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow; 132 133 // 触发UI视图更新 134 this.needsUpdate = !this.needsUpdate; 135 }) 136 }.backgroundColor(this.updateUI(this.realState)) 137 .width(200).height(500) 138 } 139} 140``` 141 142上述示例存在以下问题: 143 144- 应用程序希望控制UI更新逻辑,但在ArkUI中,UI更新的逻辑应该是由框架来检测应用程序状态变量的更改去实现。 145 146- this.needsUpdate是一个自定义的UI状态变量,应该仅应用于其绑定的UI组件。变量this.realStateArr、this.realState没有被装饰,他们的变化将不会触发UI刷新。 147 148- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realStateArr、this.realState的更新,此方法不合理且更新性能较差。 149 150【正例】 151 152要解决此问题,应将realStateArr和realState成员变量用\@State装饰。一旦完成此操作,就不再需要变量needsUpdate。 153 154 155```ts 156@Entry 157@Component 158struct CompA { 159 @State realStateArr: Array<number> = [4, 1, 3, 2]; 160 @State realState: Color = Color.Yellow; 161 build() { 162 Column({ space: 20 }) { 163 ForEach(this.realStateArr, 164 (item: Array<number>) => { 165 Text(`${item}`) 166 }) 167 Text("add item") 168 .onClick(() => { 169 // 改变realStateArr触发UI视图更新 170 this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1); 171 }) 172 Text("chg color") 173 .onClick(() => { 174 // 改变realState触发UI视图更新 175 this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow; 176 }) 177 }.backgroundColor(this.realState) 178 .width(200).height(500) 179 } 180} 181``` 182 183## 精准控制状态变量关联的组件数 184 185建议每个状态变量关联的组件数应该少于20个。精准控制状态变量关联的组件数能减少不必要的组件刷新,提高组件的刷新效率。有时开发者会将同一个状态变量绑定多个同级组件的属性,当状态变量改变时,会让这些组件做出相同的改变,这有时会造成组件的不必要刷新,如果存在某些比较复杂的组件,则会大大影响整体的性能。但是如果将这个状态变量绑定在这些同级组件的父组件上,则可以减少需要刷新的组件数,从而提高刷新的性能。 186 187【反例】 188 189```ts 190@Observed 191class Translate { 192 translateX: number = 20; 193} 194@Component 195struct Title { 196 @ObjectLink translateObj: Translate; 197 build() { 198 Row() { 199 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 200 Image($r('app.media.icon')) 201 .width(50) 202 .height(50) 203 .translate({ 204 x:this.translateObj.translateX // this.translateObj.translateX 绑定在Image和Text组件上 205 }) 206 Text("Title") 207 .fontSize(20) 208 .translate({ 209 x: this.translateObj.translateX 210 }) 211 } 212 } 213} 214@Entry 215@Component 216struct Page { 217 @State translateObj: Translate = new Translate(); 218 219 build() { 220 Column() { 221 Title({ 222 translateObj: this.translateObj 223 }) 224 Stack() { 225 } 226 .backgroundColor("black") 227 .width(200) 228 .height(400) 229 .translate({ 230 x:this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上 231 }) 232 Button("move") 233 .translate({ 234 x:this.translateObj.translateX 235 }) 236 .onClick(() => { 237 this.getUIContext().animateTo({ 238 duration: 50 239 },()=>{ 240 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 241 }) 242 }) 243 } 244 } 245} 246``` 247 248在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有的子组件其实都处于Page下的Column中,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。 249 250【正例】 251 252```ts 253@Observed 254class Translate { 255 translateX: number = 20; 256} 257@Component 258struct Title { 259 build() { 260 Row() { 261 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 262 Image($r('app.media.icon')) 263 .width(50) 264 .height(50) 265 Text("Title") 266 .fontSize(20) 267 } 268 } 269} 270@Entry 271@Component 272struct Page1 { 273 @State translateObj: Translate = new Translate(); 274 275 build() { 276 Column() { 277 Title() 278 Stack() { 279 } 280 .backgroundColor("black") 281 .width(200) 282 .height(400) 283 Button("move") 284 .onClick(() => { 285 this.getUIContext().animateTo({ 286 duration: 50 287 },()=>{ 288 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 289 }) 290 }) 291 } 292 .translate({ // 子组件Stack和Button设置了同一个translate属性,可以统一到Column上设置 293 x: this.translateObj.translateX 294 }) 295 } 296} 297``` 298 299## 合理控制对象类型状态变量关联的组件数量 300 301 302如果将一个复杂对象定义为状态变量,需要合理控制其关联的组件数。当对象中某一个成员属性发生变化时,会导致该对象关联的所有组件刷新,尽管这些组件可能并没有直接使用到该改变的属性。为了避免这种“冗余刷新”对性能产生影响,建议合理拆分该复杂对象,控制对象关联的组件数量。具体可参考[精准控制组件的更新范围](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/precisely-control-render-scope.md)和[状态管理合理使用开发指导](properly-use-state-management-to-develope.md) 两篇文章。 303 304## 查询状态变量关联的组件数 305 306在应用开发中,可以通过HiDumper查看状态变量关联的组件数,进行性能优化。具体可参考[状态变量组件定位工具实践](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/state_variable_dfx_pratice.md)。 307 308 309## 避免在for、while等循环逻辑中频繁读取状态变量 310 311在应用开发中,应避免在循环逻辑中频繁读取状态变量,而是应该放在循环外面读取。 312 313【反例】 314 315```ts 316import hilog from '@ohos.hilog'; 317 318@Entry 319@Component 320struct Index { 321 @State message: string = ''; 322 323 build() { 324 Column() { 325 Button('点击打印日志') 326 .onClick(() => { 327 for (let i = 0; i < 10; i++) { 328 hilog.info(0x0000, 'TAG', '%{public}s', this.message); 329 } 330 }) 331 .width('90%') 332 .backgroundColor(Color.Blue) 333 .fontColor(Color.White) 334 .margin({ 335 top: 10 336 }) 337 } 338 .justifyContent(FlexAlign.Start) 339 .alignItems(HorizontalAlign.Center) 340 .margin({ 341 top: 15 342 }) 343 } 344} 345``` 346 347【正例】 348 349```ts 350import hilog from '@ohos.hilog'; 351 352@Entry 353@Component 354struct Index { 355 @State message: string = ''; 356 357 build() { 358 Column() { 359 Button('点击打印日志') 360 .onClick(() => { 361 let logMessage: string = this.message; 362 for (let i = 0; i < 10; i++) { 363 hilog.info(0x0000, 'TAG', '%{public}s', logMessage); 364 } 365 }) 366 .width('90%') 367 .backgroundColor(Color.Blue) 368 .fontColor(Color.White) 369 .margin({ 370 top: 10 371 }) 372 } 373 .justifyContent(FlexAlign.Start) 374 .alignItems(HorizontalAlign.Center) 375 .margin({ 376 top: 15 377 }) 378 } 379} 380``` 381 382## 建议使用临时变量替换状态变量 383 384在应用开发中,应尽量减少对状态变量的直接赋值,通过临时变量完成数据计算操作。 385 386状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行依赖该状态变量的组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的行为,从而提高应用性能。状态变量行为可参考[@State装饰器:组件内状态](arkts-state.md)。 387 388【反例】 389 390```ts 391import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 392 393@Entry 394@Component 395struct Index { 396 @State message: string = ''; 397 398 appendMsg(newMsg: string) { 399 // 性能打点 400 hiTraceMeter.startTrace('StateVariable', 1); 401 this.message += newMsg; 402 this.message += ';'; 403 this.message += '<br/>'; 404 hiTraceMeter.finishTrace('StateVariable', 1); 405 } 406 407 build() { 408 Column() { 409 Button('点击打印日志') 410 .onClick(() => { 411 this.appendMsg('操作状态变量'); 412 }) 413 .width('90%') 414 .backgroundColor(Color.Blue) 415 .fontColor(Color.White) 416 .margin({ 417 top: 10 418 }) 419 } 420 .justifyContent(FlexAlign.Start) 421 .alignItems(HorizontalAlign.Center) 422 .margin({ 423 top: 15 424 }) 425 } 426} 427``` 428 429直接操作状态变量,三次触发计算函数,运行耗时结果如下 430 431 432 433【正例】 434 435```ts 436import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 437 438@Entry 439@Component 440struct Index { 441 @State message: string = ''; 442 443 appendMsg(newMsg: string) { 444 // 性能打点 445 hiTraceMeter.startTrace('TemporaryVariable', 2); 446 let message = this.message; 447 message += newMsg; 448 message += ';'; 449 message += '<br/>'; 450 this.message = message; 451 hiTraceMeter.finishTrace('TemporaryVariable', 2); 452 } 453 454 build() { 455 Column() { 456 Button('点击打印日志') 457 .onClick(() => { 458 this.appendMsg('操作临时变量'); 459 }) 460 .width('90%') 461 .backgroundColor(Color.Blue) 462 .fontColor(Color.White) 463 .margin({ 464 top: 10 465 }) 466 } 467 .justifyContent(FlexAlign.Start) 468 .alignItems(HorizontalAlign.Center) 469 .margin({ 470 top: 15 471 }) 472 } 473} 474``` 475 476使用临时变量取代状态变量的计算,三次触发计算函数,运行耗时结果如下 477 478 479 480【总结】 481| **计算方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 482| ------ | ------- | ------------------------------------- | 483| 直接操作状态变量 | 1.01ms | 增加了ArkUI不必要的查询和渲染行为,导致性能劣化 | 484| 使用临时变量计算 | 0.63ms | 减少了ArkUI不必要的行为,优化性能 |