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.realStateArrthis.realState没有被装饰,他们的变化将不会触发UI刷新。
147
148- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realStateArrthis.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  build() {
219    Column() {
220      Title({
221        translateObj: this.translateObj
222      })
223      Stack() {
224      }
225      .backgroundColor("black")
226      .width(200)
227      .height(400)
228      .translate({
229        x:this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上
230      })
231      Button("move")
232        .translate({
233          x:this.translateObj.translateX
234        })
235        .onClick(() => {
236          animateTo({
237            duration: 50
238          },()=>{
239            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
240          })
241        })
242    }
243  }
244}
245```
246
247在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有的子组件其实都处于Page下的Column中,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。
248
249【正例】
250
251```ts
252@Observed
253class Translate {
254  translateX: number = 20;
255}
256@Component
257struct Title {
258  build() {
259    Row() {
260      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
261      Image($r('app.media.icon'))
262        .width(50)
263        .height(50)
264      Text("Title")
265        .fontSize(20)
266    }
267  }
268}
269@Entry
270@Component
271struct Page1 {
272  @State translateObj: Translate = new Translate();
273  build() {
274    Column() {
275      Title()
276      Stack() {
277      }
278      .backgroundColor("black")
279      .width(200)
280      .height(400)
281      Button("move")
282        .onClick(() => {
283          animateTo({
284            duration: 50
285          },()=>{
286            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
287          })
288        })
289    }
290    .translate({ // 子组件Stack和Button设置了同一个translate属性,可以统一到Column上设置
291      x: this.translateObj.translateX
292    })
293  }
294}
295```
296
297## 合理控制对象类型状态变量关联的组件数量
298
299
300如果将一个复杂对象定义为状态变量,需要合理控制其关联的组件数。当对象中某一个成员属性发生变化时,会导致该对象关联的所有组件刷新,尽管这些组件可能并没有直接使用到该改变的属性。为了避免这种“冗余刷新”对性能产生影响,建议合理拆分该复杂对象,控制对象关联的组件数量。具体可参考[精准控制组件的更新范围](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/precisely-control-render-scope.md)和[状态管理合理使用开发指导](properly-use-state-management-to-develope.md) 两篇文章。
301
302## 查询状态变量关联的组件数
303
304在应用开发中,可以通过HiDumper查看状态变量关联的组件数,进行性能优化。具体可参考[状态变量组件定位工具实践](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/state_variable_dfx_pratice.md)305
306
307## 避免在for、while等循环逻辑中频繁读取状态变量
308
309在应用开发中,应避免在循环逻辑中频繁读取状态变量,而是应该放在循环外面读取。
310
311【反例】
312
313```ts
314import hilog from '@ohos.hilog';
315
316@Entry
317@Component
318struct Index {
319  @State message: string = '';
320
321  build() {
322    Column() {
323      Button('点击打印日志')
324        .onClick(() => {
325          for (let i = 0; i < 10; i++) {
326            hilog.info(0x0000, 'TAG', '%{public}s', this.message);
327          }
328        })
329        .width('90%')
330        .backgroundColor(Color.Blue)
331        .fontColor(Color.White)
332        .margin({
333          top: 10
334        })
335    }
336    .justifyContent(FlexAlign.Start)
337    .alignItems(HorizontalAlign.Center)
338    .margin({
339      top: 15
340    })
341  }
342}
343```
344
345【正例】
346
347```ts
348import hilog from '@ohos.hilog';
349
350@Entry
351@Component
352struct Index {
353  @State message: string = '';
354
355  build() {
356    Column() {
357      Button('点击打印日志')
358        .onClick(() => {
359          let logMessage: string = this.message;
360          for (let i = 0; i < 10; i++) {
361            hilog.info(0x0000, 'TAG', '%{public}s', logMessage);
362          }
363        })
364        .width('90%')
365        .backgroundColor(Color.Blue)
366        .fontColor(Color.White)
367        .margin({
368          top: 10
369        })
370    }
371    .justifyContent(FlexAlign.Start)
372    .alignItems(HorizontalAlign.Center)
373    .margin({
374      top: 15
375    })
376  }
377}
378```
379
380## 建议使用临时变量替换状态变量
381
382在应用开发中,应尽量减少对状态变量的直接赋值,通过临时变量完成数据计算操作。
383
384状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行依赖该状态变量的组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的行为,从而提高应用性能。状态变量行为可参考[@State装饰器:组件内状态](arkts-state.md)。
385
386【反例】
387
388```ts
389import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
390
391@Entry
392@Component
393struct Index {
394  @State message: string = '';
395
396  appendMsg(newMsg: string) {
397    // 性能打点
398    hiTraceMeter.startTrace('StateVariable', 1);
399    this.message += newMsg;
400    this.message += ';';
401    this.message += '<br/>';
402    hiTraceMeter.finishTrace('StateVariable', 1);
403  }
404
405  build() {
406    Column() {
407      Button('点击打印日志')
408        .onClick(() => {
409          this.appendMsg('操作状态变量');
410        })
411        .width('90%')
412        .backgroundColor(Color.Blue)
413        .fontColor(Color.White)
414        .margin({
415          top: 10
416        })
417    }
418    .justifyContent(FlexAlign.Start)
419    .alignItems(HorizontalAlign.Center)
420    .margin({
421      top: 15
422    })
423  }
424}
425```
426
427直接操作状态变量,三次触发计算函数,运行耗时结果如下
428
429![](figures/hp_arkui_use_state_var.png)
430
431【正例】
432
433```ts
434import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
435
436@Entry
437@Component
438struct Index {
439  @State message: string = '';
440
441  appendMsg(newMsg: string) {
442    // 性能打点
443    hiTraceMeter.startTrace('TemporaryVariable', 2);
444    let message = this.message;
445    message += newMsg;
446    message += ';';
447    message += '<br/>';
448    this.message = message;
449    hiTraceMeter.finishTrace('TemporaryVariable', 2);
450  }
451
452  build() {
453    Column() {
454      Button('点击打印日志')
455        .onClick(() => {
456          this.appendMsg('操作临时变量');
457        })
458        .width('90%')
459        .backgroundColor(Color.Blue)
460        .fontColor(Color.White)
461        .margin({
462          top: 10
463        })
464    }
465    .justifyContent(FlexAlign.Start)
466    .alignItems(HorizontalAlign.Center)
467    .margin({
468      top: 15
469    })
470  }
471}
472```
473
474使用临时变量取代状态变量的计算,三次触发计算函数,运行耗时结果如下
475
476![](figures/hp_arkui_use_local_var.png)
477
478【总结】
479| **计算方式** | **耗时(局限不同设备和场景,数据仅供参考)**  | **说明** |
480| ------ | ------- | ------------------------------------- |
481| 直接操作状态变量  | 1.01ms | 增加了ArkUI不必要的查询和渲染行为,导致性能劣化 |
482| 使用临时变量计算  | 0.63ms | 减少了ArkUI不必要的行为,优化性能 |