• 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 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
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![](figures/hp_arkui_use_state_var.png)
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![](figures/hp_arkui_use_local_var.png)
479
480【总结】
481| **计算方式** | **耗时(局限不同设备和场景,数据仅供参考)**  | **说明** |
482| ------ | ------- | ------------------------------------- |
483| 直接操作状态变量  | 1.01ms | 增加了ArkUI不必要的查询和渲染行为,导致性能劣化 |
484| 使用临时变量计算  | 0.63ms | 减少了ArkUI不必要的行为,优化性能 |