一、开场白:你的组件为啥老瞎刷新?

咱们写 HarmonyOS 应用,有没有碰过这事儿:

明明只改了一个小组件的状态,结果一大片组件都跟着刷新了,界面卡得一批。

这就是组件冗余刷新

ArkUI 里,变量被 @State、@Prop 这些装饰器一装饰,就成了状态变量。状态变量一变,用了这个变量的 UI 组件就得刷新。

但这里有个坑——你要是状态变量用得不合理,就会有一堆组件瞎刷新

今天我们就教你们两招:

  1. hidumper 工具定位谁在瞎刷新
  2. @Observed/@ObjectLink 优化状态变量,让该刷新的刷新,不该刷新的别动

咱们边讲边实操,保证你们看完就能用。


二、先看个问题:点这个按钮,那个组件为啥刷新?

我们举个例子,你们就明白啥叫冗余刷新了。

有个 ComponentA 组件,里面有两个按钮:Move 和 Scale。

  • 点 Move 按钮 → 改 translateY 属性 → 组件应该上下移动
  • 点 Scale 按钮 → 改 scaleX 属性 → 组件应该缩放

还有个 SpecialImage 组件,它只关心 scaleXscaleY,跟 translateY 半毛钱关系没有。

但问题来了

点 Move 按钮的时候,SpecialImage 居然也刷新了!

看上图,点 Move 的时候,SpecialImage 也跟着转,纯属瞎刷新。

为啥会这样?

因为 ComponentASpecialImage 共享了一个 uiStyle 对象,这个对象里既有 translateY 也有 scaleX

你改 translateY,整个 uiStyle 对象就变了,SpecialImage 一看:“哎,我订阅的 uiStyle 变了”,然后就刷新了。

冤不冤?太冤了。


三、用 hidumper 抓出谁是罪魁祸首

光说没用,咱们得抓出到底是谁在瞎刷新。

HarmonyOS 给了咱们一个神器——hidumper。这玩意儿能告诉你:

  • 每个组件有哪些状态变量
  • 这些状态变量被哪些组件订阅
  • 改这个状态变量会影响哪些组件

咱们一步步来。

步骤 1:获取应用窗口 Id

首先,在设备上打开应用,进入 ComponentA 组件所在的页面。

然后执行这个命令:

hdc shell "hidumper -s WindowManagerService -a '-a'"

输出里找你的应用包名,比如我们的示例应用包名是 performancelibrary,找到对应的窗口名 performancelibrary0,它的 WinId 就是窗口 Id。

或者看 Focus window 的值,应用在前台时,这个值就是窗口 Id。

我们的示例窗口 Id 是 11

步骤 2:获取自定义组件树

有了窗口 Id,咱们看看应用里有哪些组件:

hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -viewHierarchy -r'"

输出是这样的:

-----------------ViewPUHierarchy-----------------
[-viewHierarchy, viewId=4, isRecursive=true]

|--DFXStateBeforeOptimization[4]ViewPU {isViewActive: true, isDeleting_: false}
  |--ComponentA[6]ViewPU {isViewActive: true, isDeleting_: false}
    |--SpecialImage[8]ViewPU {isViewActive: true, isDeleting_: false}

找到 ComponentA,它后面的 [6] 就是节点 Id。记住这个 6,后面要用。

步骤 3:获取状态变量信息

现在看看 ComponentA 里有哪些状态变量:

hdc shell "hidumper -s WindowManagerService -a '-w 11 -jsdump -stateVariables -viewId=6'"

输出:

--------------ViewPUState Variables--------------
[-stateVariables, viewId=6, isRecursive=false]

|--ComponentA[6]
  @Link 'uiStyle'[-1]
  |--Owned by @Component 'ComponentA'[6]
  |--Sync peers: {
    @Link 'specialImageUiStyle'[-2] <@Component 'SpecialImage'[8]>
  }
  dependencies: variable assignment affects elmtIds: Column[9], Image[10]
  |--Dependent elements: Column[9], Image[10]; @Component 'SpecialImage'[8], Image[18]

步骤 4:分析谁在瞎刷新

看上面这个输出,咱们来解读一下:

@Link 'uiStyle':这是 ComponentA 的状态变量

Sync peers:谁订阅了这个变量

  • @Link 'specialImageUiStyle'SpecialImage[8] 组件里订阅了

Dependent elements:改这个变量会影响哪些组件

  • Column[9]Image[10]SpecialImage[8]Image[18] 都会刷新

问题根源找到了

SpecialImage 明明只用 uiStyle 里的 scaleXscaleY,但你改 translateY 的时候,整个 uiStyle 对象都变了,SpecialImage 也跟着刷新。

这就好比你家邻居装修,你家也跟着停电,合理吗?不合理。


四、解决方案:把大对象拆小,各用各的

咋解决?

我们把 uiStyle 拆开:

  • scaleStyle:放 scaleXscaleY,给 SpecialImage
  • translateStyle:放 translateXtranslateY,给其他组件用

这样改 translateStyle 的时候,SpecialImage 就不会刷新了。

优化后的代码

// 常量声明
const animationDuration: number = 500;
const opacityChangeValue: number = 0.1;
const opacityChangeRange: number = 1;
const translateYChangeValue: number = 180;
const translateYChangeRange: number = 250;
const scaleXChangeValue: number = 0.6;
const scaleXChangeRange: number = 0.8;

// 样式属性类,嵌套 ScaleStyle、TranslateStyle
@Observed
class UIStyle {
  translateStyle: TranslateStyle = new TranslateStyle();
  scaleStyle: ScaleStyle = new ScaleStyle();
}

// 缩放属性类
@Observed
class ScaleStyle {
  public scaleX: number = 0.3;
  public scaleY: number = 0.3;
}

// 位移属性类
@Observed
class TranslateStyle {
  public translateX: number = 0;
  public translateY: number = 0;
}

@Component
struct ComponentA {
  @ObjectLink scaleStyle: ScaleStyle;
  @ObjectLink translateStyle: TranslateStyle;

  build() {
    Column() {
      SpecialImage({
        specialImageScaleStyle: this.scaleStyle
      })
      // 其他 UI 组件
      Column() {
        Image($r('app.media.startIcon'))
          .height('150vp')
          .width('150vp')
          .scale({
            x: this.scaleStyle.scaleX,
            y: this.scaleStyle.scaleY
          })
        Text('Hello World')
          .fontWeight(FontWeight.Bold)
      }
      .translate({
        x: this.translateStyle.translateX,
        y: this.translateStyle.translateY
      })
      .width('95%')
      .height('200vp')
      .margin({
        top: '10vp',
        left: '15vp',
        right: '15vp'
      })
      .borderRadius('16vp')
      .backgroundColor(Color.White)
      
      // 按钮点击回调
      Column() {
        Button('Move')
          .width('80%')
          .onClick(() => {
            this.getUIContext().animateTo({ duration: animationDuration }, () => {
              this.translateStyle.translateY =
                (this.translateStyle.translateY + translateYChangeValue) % translateYChangeRange;
            })
          })
        Button('Scale')
          .width('80%')
          .onClick(() => {
            this.scaleStyle.scaleX = (this.scaleStyle.scaleX + scaleXChangeValue) % scaleXChangeRange;
          })
          .margin({
            top: '10vp',
            left: '15vp',
            right: '15vp'
          })
      }
      .height('35%')
      .justifyContent(FlexAlign.End)
      .width('100%')
    }
  }
}

@Component
struct SpecialImage {
  @Link specialImageScaleStyle: ScaleStyle;
  private opacityNum: number = 0.5;

  private isRenderSpecialImage(): number {
    this.opacityNum = (this.opacityNum + opacityChangeValue) % opacityChangeRange;
    return this.opacityNum;
  }

  build() {
    Column() {
      Image($r('app.media.startIcon'))
        .size({ width: 78, height: 78 })
        .scale({
          x: this.specialImageScaleStyle.scaleX,
          y: this.specialImageScaleStyle.scaleY
        })
        .opacity(this.isRenderSpecialImage())
      Text("SpecialImage")
        .fontWeight(FontWeight.Bold)
    }
    .width('95%')
    .margin({
      top: '10vp',
      left: '15vp',
      right: '15vp'
    })
    .borderRadius('16vp')
    .height('200vp')
    .backgroundColor(Color.White)
  }
}

@Entry
@Component
struct DFXStateAfterOptimization {
  @State uiStyle: UIStyle = new UIStyle();

  build() {
    Stack() {
      ComponentA({
        scaleStyle: this.uiStyle.scaleStyle,
        translateStyle: this.uiStyle.translateStyle,
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xDCDCDC)
  }
}

关键改动

  1. @Observed 装饰三个类:UIStyleScaleStyleTranslateStyle
  2. ComponentA@ObjectLink 接收 scaleStyletranslateStyle
  3. SpecialImage 只接收 scaleStyle,跟 translateStyle 彻底没关系

优化后的效果

现在点 Move 按钮,SpecialImage 不刷新了!

点 Scale 按钮,SpecialImage 才刷新。

这就对了,谁用谁刷新,别人别跟着瞎掺和。

再次用 hidumper 验证

咱们再用 hidumper 看看优化后的状态变量信息:

--------------ViewPUState Variables--------------
[-stateVariables, viewId=6, isRecursive=false]

|--ComponentA[6]
  @ObjectLink 'scaleStyle'[-1]
  |--Owned by @Component 'ComponentA'[6]
  |--Sync peers: {
    @Link 'specialImageScaleStyle'[-3] <@Component 'SpecialImage'[8]>
  }
  dependencies: variable assignment affects elmtIds: Image[10]
  |--Dependent elements: Image[10]; @Component 'SpecialImage'[8], Image[18]
  @ObjectLink 'translateStyle'[-2]
  |--Owned by @Component 'ComponentA'[6]
  |--Sync peers: none
  dependencies: variable assignment affects elmtIds: Column[9]
  |--Dependent elements: Column[9]

看:

  • scaleStyle 变了 → SpecialImage[8]Image[18] 刷新 ✅
  • translateStyle 变了 → 只有 Column[9] 刷新 ✅

translateStyleSync peersnone,说明没组件订阅它,SpecialImage 彻底解脱了。


五、总结一下

组件冗余刷新这事儿,我们总结了三步走:

1. 用 hidumper 定位问题

# 获取窗口 Id
hdc shell "hidumper -s WindowManagerService -a '-a'"

# 获取组件树
hdc shell "hidumper -s WindowManagerService -a '-w 窗口 Id -jsdump -viewHierarchy -r'"

# 获取状态变量信息
hdc shell "hidumper -s WindowManagerService -a '-w 窗口 Id -jsdump -stateVariables -viewId=组件节点 Id'"

2. 分析状态变量影响范围

看输出里的两个关键字段:

  • Sync peers:谁订阅了这个状态变量
  • Dependent elements:改这个状态变量会影响哪些组件

要是发现某个组件明明不用这个变量,却在 Dependent elements 里,那就是冗余刷新了。

3. 优化方案

  • 把大对象拆成小对象,按功能提取属性
  • @Observed 装饰 Class
  • @ObjectLink 代替 @Link 传递嵌套对象
  • 只传递组件真正需要的状态变量

最后一句

状态变量拆得越细,冗余刷新就越少,性能就越好。你们写代码的时候多想想,别图省事把一堆属性塞一个对象里。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐