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 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 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 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 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 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 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