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