1# 状态变量组件定位工具实践
2
3## 概述
4
5自定义组件中的变量被状态装饰器(@State,@Prop等)装饰后成为状态变量,而状态变量的改变会引起使用该变量的UI组件渲染刷新。状态变量的不合理使用可能会带来冗余刷新等性能问题。开发者可以使用状态变量组件定位工具获取状态管理相关信息,例如自定义组件拥有的状态变量、状态变量的同步对象和关联组件等,了解状态变量影响UI的范围,写出高性能应用代码。
6
7本文将通过场景示例为开发者提供状态变量组件定位工具的实践指导,并对工具相关的调试命令和输出结果作解释说明。
8
9## 使用流程
10
11状态变量组件定位工具是通过使用HiDumper工具中特定的命令来获取组件和状态管理相关的信息。开发者可以使用工具查看指定自定义组件拥有的状态变量信息,了解其中各个状态变量影响的组件范围。
12
13工具的使用流程可以分为以下几步:
14
151、设备上打开组件所在页面。
16
172、获取应用的窗口Id。
18
193、基于应用窗口Id获取应用的自定义组件树,找到目标组件和它的节点Id。
20
214、根据上一步获取的组件节点Id获取组件拥有的状态变量信息。
22
235、找到目标状态变量,查看它影响的组件范围。
24
25在明确目标组件包含的状态变量和变量影响的组件刷新范围后,开发者可以根据需求分析是否存在状态变量的不合理使用,并对相应代码分析和优化。
26
27## 场景示例
28
29下面通过一个点击按钮更改状态变量引起组件刷新的场景示例,为开发者提供工具的实践指导。场景示例仅展示部分关键代码,完整代码请访问[示例代码](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/feature/DFXStateManagement/src/main/ets/view/DFXStateBeforeOptimization.ets)30
31在以下代码中,创建了自定义组件ComponentA、SpecialImage,每个组件都拥有一些状态变量和UI组件。组件ComponentA中存在Move和Scale两个按钮,在按钮的点击回调中改变状态变量的值刷新相应的组件。
32```javascript
33// feature/DFXStateManagement/src/main/ets/view/DFXStateBeforeOptimization.ets
34
35const animationDuration: number = 500; // move动画时长
36const opacityChangeValue: number = 0.1; // opacity每次变化的值
37const opacityChangeRange: number = 1; // opacity变化的范围
38const translateYChangeValue: number = 180; // translateY每次变化的值
39const translateYChangeRange: number = 250; // translateY变化的范围
40const scaleXChangeValue: number = 0.6; // scaleX每次变化的值
41const scaleXChangeRange: number = 0.8; // scaleX每次变化的值
42// 样式属性类
43class UIStyle {
44  public translateX: number = 0;
45  public translateY: number = 0;
46  public scaleX: number = 0.3;
47  public scaleY: number = 0.3;
48}
49@Component
50struct ComponentA {
51  @Link uiStyle: UIStyle; // uiStyle的属性被多个组件使用
52  build() {
53    Column() {
54      // 使用状态变量的组件
55      SpecialImage({ specialImageUiStyle: this.uiStyle })
56      Stack() {
57        Column() {
58          Image($r('app.media.icon'))
59            .scale({
60              x: this.uiStyle.scaleX,
61              y: this.uiStyle.scaleY
62            })
63        }
64        Stack() {
65          Text('Hello World')
66        }
67      }
68      .translate({
69        x: this.uiStyle.translateX,
70        y: this.uiStyle.translateY
71      })
72
73      // 通过按钮点击回调修改状态变量的值,引起相应的组件刷新
74      Column() {
75        Button('Move')
76          .onClick(() => {
77            animateTo({ duration: animationDuration }, () => {
78              this.uiStyle.translateY = (this.uiStyle.translateY + translateYChangeValue) % translateYChangeRange;
79            })
80          })
81        Button('Scale')
82          .onClick(() => {
83            this.uiStyle.scaleX = (this.uiStyle.scaleX + scaleXChangeValue) % scaleXChangeRange;
84          })
85      }
86    }
87  }
88}
89@Component
90struct SpecialImage {
91  @Link specialImageUiStyle: UIStyle;
92  private opacityNum: number = 0.5; // 默认透明度
93  private isRenderSpecialImage(): number {
94    // Image每次渲染时透明度增加0.1, 在0-1之间循环
95    this.opacityNum = (this.opacityNum + opacityChangeValue) % opacityChangeRange;
96    return this.opacityNum;
97  }
98  build() {
99    Column() {
100      Image($r('app.media.icon'))
101        .scale({
102          x: this.specialImageUiStyle.scaleX,
103          y: this.specialImageUiStyle.scaleY
104        })
105        .opacity(this.isRenderSpecialImage())
106      Text("SpecialImage")
107    }
108  }
109}
110// 页面根组件,ComponentA的父组件
111```
112
113运行上述示例并分别点击按钮,可以看到点击Move按钮和Scale按钮时组件SpecialImage都出现了刷新,运行效果图如下。
114
115![](./figures/state_viariable_dfx_pratice_pic1.gif)
116
117下面以自定义组件ComponentA和其中的状态变量uiStyle为例介绍工具的使用过程。
118
1191、首先在设备上打开应用,进入ComponentA组件所在的页面。
120
1212、使用以下命令获取示例应用的窗口Id。当前运行的示例应用包名为performancelibrary,可以在输出结果中找到对应窗口名performancelibrary0的WinId,即为应用的窗口Id。或者当应用正处于前台运行时,Focus window的值就是应用的窗口Id。此处示例应用的窗口Id为11,后面的流程中使用的命令都需要指定窗口Id。
122```shell
123hdc shell "hidumper -s WindowManagerService -a '-a'"
124```
125![](./figures/state_viariable_dfx_pratice_pic2.png)
126
1273、基于上一步获取的窗口Id 11,使用-viewHierarchy命令携带-r 参数递归打印应用的自定义组件树。从结果中找到目标组件ComponentA,后面括号中的内容即为组件ComponentA的节点Id 70。
128```shell
129hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy -r'"
130```
131```shell
132-----------------ViewPU Hierarchy-----------------
133[-viewHierarchy, viewId=4, isRecursive=true]
134|--Index[4]
135-----------------ViewPU Hierarchy-----------------
136[-viewHierarchy, viewId=53, isRecursive=true]
137|--DFXStateManagementPage[53]
138  |--DFXStateManagementHome[55]
139-----------------ViewPU Hierarchy-----------------
140[-viewHierarchy, viewId=65, isRecursive=true]
141|--DFXStateBeforeOptimizationPage[65]
142  |--DFXStateBeforeOptimization[67]
143    |--ComponentA[70]
144      |--SpecialImage[73]
145```
1464、使用命令-stateVariables携带参数-viewId(参数的值为ComponentA的节点Id)获取自定义组件ComponentA中的状态变量信息。结果显示ComponentA拥有@Link/@Consume类型的状态变量uiStyle。每条状态变量的详细信息都包含状态变量的所属组件、同步对象和关联组件。
147```shell
148hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -stateVariables -viewId=70'"
149```
150```shell
151--------------ViewPU State Variables--------------
152[-stateVariables, viewId=70, isRecursive=false]
153|--ComponentA[70]
154  @Link/@Consume (class SynchedPropertyTwoWayPU) 'uiStyle'[71]
155  |--Owned by @Component 'ComponentA'[70]
156  |--Sync peers: {
157    @Link/@Consume (class SynchedPropertyTwoWayPU) 'specialImageUiStyle'[74] <@Component 'SpecialImage'[73]>
158  }
159  |--Dependent components: 2 elmtIds: 'Stack[75]', 'Image[77]'
160```
1615、以状态变量uiStyle为例。
162
163① Sync peers表示uiStyle在自定义组件SpecialImage中存在@Link/@Consume类型的状态变量specialImageUiStyle订阅数据变化。
164
165② Dependent components表示在ComponentA组件中存在组件Stack[79]和Image[81]使用了状态变量uiStyle,关联组件的数量为2。
166
167所以当uiStyle变化时,影响的组件范围为自定义组件SpecialImage以及系统组件Stack[79]和Image[81]。
168
169![](./figures/state_viariable_dfx_pratice_pic3.png)
170
171示例中组件SpecialImage仅使用了uiStyle传递到specialImageUiStyle中的属性scaleX、scaleY,但是点击Move按钮修改uiStyle中的属性translateY时引起的uiStyle变化也会导致组件SpecialImage的刷新,所以可以将uiStyle中的属性scaleX、scaleY提取到状态变量scaleStyle中,属性translateX和translateY提取到状态变量translateStyle中,仅传递scaleStyle给组件SpecialImage,避免不必要的刷新。
172
173由于提取后存在Class的嵌套,所以需要使用@Observed/@ObjectLink装饰器装饰相应的Class和状态变量。修改后的部分代码如下,完整代码可访问[示例代码](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/feature/DFXStateManagement/src/main/ets/view/DFXStateAfterOptimization.ets)获取。
174```javascript
175// feature/DFXStateManagement/src/main/ets/view/DFXStateAfterOptimization.ets
176
177// 常量声明
178// ...
179// 样式属性类,嵌套ScaleStyle, TranslateStyle
180@Observed
181class UIStyle {
182  translateStyle: TranslateStyle = new TranslateStyle();
183  scaleStyle: ScaleStyle = new ScaleStyle();
184}
185// 缩放属性类
186@Observed
187class ScaleStyle {
188  public scaleX: number = 0.3;
189  public scaleY: number = 0.3;
190}
191// 位移属性类
192@Observed
193class TranslateStyle {
194  public translateX: number = 0;
195  public translateY: number = 0;
196}
197@Component
198struct ComponentA {
199  @ObjectLink scaleStyle: ScaleStyle;
200  @ObjectLink translateStyle: TranslateStyle;
201
202  build() {
203    Column() {
204      SpecialImage({
205        specialImageScaleStyle: this.scaleStyle
206      })
207      // 其他UI组件
208    }
209  }
210}
211
212@Component
213struct SpecialImage {
214  @Link specialImageScaleStyle: ScaleStyle;
215  // isRenderSpecialImage函数
216  build() {
217    Column() {
218      Image($r('app.media.icon'))
219        .scale({
220          x: this.specialImageScaleStyle.scaleX,
221          y: this.specialImageScaleStyle.scaleY
222        })
223        .opacity(this.isRenderSpecialImage())
224      Text("SpecialImage")
225    }
226  }
227}
228// 页面根组件,ComponentA的父组件
229```
230
231修改后的示例运行效果图如下,只有点击Scale按钮时SpecialImage产生刷新现象,点击Move按钮时SpecialImage不会刷新。
232
233![](./figures/state_viariable_dfx_pratice_pic4.gif)
234
235可以使用上文步骤再次获取ComponentA组件的状态变量信息如下,可以看到ComponentA中状态变量scaleStyle影响组件SpecialImage[74]和Image[78],状态变量translateStyle影响组件Stack[76],translateStyle的变化不会再导致SpecialImage的刷新。
236```shell
237--------------ViewPU State Variables--------------
238[-stateVariables, viewId=70, isRecursive=false]
239
240|--ComponentA[70]
241  @ObjectLink (class SynchedPropertyNestedObjectPU) 'scaleStyle'[71]
242  |--Owned by @Component 'ComponentA'[70]
243  |--Sync peers: {
244    @Link/@Consume (class SynchedPropertyTwoWayPU) 'specialImageScaleStyle'[75] <@Component 'SpecialImage'[74]>
245  }
246  |--Dependent components: 1 elmtIds: 'Image[78]'
247  @ObjectLink (class SynchedPropertyNestedObjectPU) 'translateStyle'[72]
248  |--Owned by @Component 'ComponentA'[70]
249  |--Sync peers: none
250  |--Dependent components: 1 elmtIds: 'Stack[76]'
251```
252## 调试命令和输出详解
253
254下面通过一个自定义组件三层嵌套的简单示例程序,对工具的调试命令和输出结果作详细说明。
255```javascript
256@Entry
257@Component
258struct Index {
259  @State indexMessage: string = 'Hello World';
260  build() {
261    Row() {
262      Column() {
263        Text(this.indexMessage)
264        ComponentA({ componentAMessage: this.indexMessage })
265      }
266      .width('100%')
267    }
268    .height('100%')
269  }
270}
271@Component
272struct ComponentA {
273  @Link componentAMessage: string;
274  build() {
275    Column() {
276      ComponentB({ componentBMessage: this.componentAMessage })
277    }
278  }
279}
280@Component
281struct ComponentB {
282  @Link componentBMessage: string;
283  build() {
284    Column() {
285      Text(this.componentBMessage)
286    }
287  }
288}
289```
2901、查看应用窗口Id。可以通过窗口列表中应用的WindowName(示例应用的包名为dfxdemo,默认的WindowName为dfxdemo0)找到其WinId,即应用窗口Id。结果中的Focus window为当前界面展示的窗口Id。当应用处于前台运行时,Focus window的值即为应用窗口Id。
291```shell
292hdc shell "hidumper -s WindowManagerService -a '-a'"
293```
294![](./figures/state_viariable_dfx_pratice_pic5.png)
295
2962、打印自定义组件树。
297
298默认只打印根节点和它子级的自定义组件。其中 11 表示查看的窗口Id,可使用查看应用窗口Id命令获取。结果中形如Index[4]格式的,前面为自定义组件的名称,[]中的数字为组件的节点Id。
299```shell
300hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy'"
301```
302```shell
303-----------------ViewPU Hierarchy-----------------
304[-viewHierarchy, viewId=4, isRecursive=false]
305|--Index[4]
306  |--ComponentA[9]
307```
308携带-viewId参数可以打印指定viewId的自定义组件和该组件子级的自定义组件。只有自定义组件的节点Id可以作为参数-viewId的值使用。以-viewId=9为例,可以获取到viewId为9的自定义组件ComponentA和它子级的自定义组件ComponentB。
309```shell
310hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy -viewId=9'"
311```
312```shell
313-----------------ViewPU Hierarchy-----------------
314[-viewHierarchy, viewId=9, isRecursive=false]
315|--ComponentA[9]
316  |--ComponentB[12]
317```
318携带-r参数从根节点开始递归打印自定义组件树。例如下面命令的输出结果为根节点Index和它拥有的每一级自定义组件,以树状结构展示。
319```shell
320hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy -r'"
321```
322```shell
323-----------------ViewPU Hierarchy-----------------
324[-viewHierarchy, viewId=4, isRecursive=true]
325|--Index[4]
326  |--ComponentA[9]
327    |--ComponentB[12]
328```
3293、打印自定义组件的状态变量信息,每条信息包含该变量的所属组件、Sync peers(同步对象)和Dependent components(关联组件)。当该状态变量改变时,它的Dependent components和Sync peers的Dependent components都是脏节点。默认打印根节点。
330```shell
331hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -stateVariables'"
332```
333```shell
334--------------ViewPU State Variables--------------
335--Index[4]
336  @State/@Provide (class ObservedPropertyPU) 'indexMessage'[5]
337  |--Owned by @Component 'Index'[4]
338  |--Sync peers: {
339    @Link/@Consume (class SynchedPropertyTwoWayPU) 'componentAMessage'[9] <@Component 'ComponentA'[8]>
340  }
341  |--Dependent components: 1 elmtIds: 'Text[8]'
342```
343- @State/@Provide:状态变量的装饰器类型。
344- 'message'[5]:状态变量的名称和节点Id,但是不能使用-viewId=5来获取相应的dump信息。
345- Owned by @Component 'Index'[4]:状态变量所属组件。
346- Sync peers: 要同步的其他状态变量和其所属的组件。
347- Dependent components:当前状态变量在此组件中关联的节点Id,即依赖该变量的系统或自定义组件的节点Id。
348
349**注意:**
350
351-stateVariables只支持打印指定viewId的状态变量信息,不支持递归打印。所以只能获取单个自定义组件的状态变量信息进行逐级分析,目前无法从全局查看某个状态变量影响到的所有组件。
352
3534、打印所有信息,包含自定义组件树和状态变量信息。未指定节点时默认打印根节点,携带-viewId参数打印指定节点信息,携带-r参数递归打印。
354```shell
355hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -dumpAll'"
356```
357输出结果如下图:
358
359![](./figures/state_viariable_dfx_pratice_pic6.png)
360
3611、自定义组件树,对应命令-viewHierarchy。
362
3632、状态变量信息,对应命令-stateVariables。
364
365dumpAll命令携带-r和-viewId参数时,输出结果中对应各个命令的部分与单独使用该命令携带相应参数时的结果相同。
366
367## 参考资料
368
369[场景示例代码](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Performance/PerformanceLibrary/feature/DFXStateManagement/src/main/ets/view)
370
371[使用HiDumper命令行工具优化性能](performance-optimization-using-hidumper.md)
372
373[精准控制组件的更新范围](precisely-control-render-scope.md)
374