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