HarmonyOS:状态管理优秀实践

一、前言

为了帮助应用程序开发人员提高其应用程序质量,特别是在高效的状态管理方面。本章节面向开发者提供了多个在开发ArkUI应用中常见的低效开发的场景,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如何正确地在应用开发中使用状态变量,进行高性能开发。

二、使用@ObjectLink代替@Prop减少不必要的深拷贝

在应用开发中,开发者经常会进行父子组件的数值传递,而在不会改变子组件内状态变量值的情况下,使用@Prop装饰状态变量会导致组件创建的耗时增加,从而影响一部分性能。

【反例】

@Observed
class MyClass6 {
  public num: number = 0;

  constructor(num: number) {
    this.num = num;
  }
}

@Component
struct PropChild6 {
  @Prop testClass: MyClass6; // @Prop 装饰状态变量会深拷贝

  build() {
    Text(`PropChild testNum ${this.testClass.num}`)
      .fontColor(Color.Black)
      .fontWeight(FontWeight.Bold)
      .fontSize(20)
  }
}

@Entry
@Component
struct TestStateMangager {
  @State message: string = '状态管理优秀实践';
  @State testClassArr: MyClass6[] = [new MyClass6(3)];

  build() {
    Column({ space: 10 }) {
      Text(this.message)
        .id('TestStateMangagerHelloWorld')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(`TestStateMangager testNum is ${this.testClassArr[0].num} `).fontColor(Color.Black)
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .onClick(() => {
          this.testClassArr[0].num += 1;
          console.log(`点击了 TestStateMangager testNum this.testClassArr[0].num = ${this.testClassArr[0].num}`)
        })
      // PropChild6没有改变@Prop testClassArr: MyClass6的值,所以这时最优的选择是使用@ObjectLink
      PropChild6({ testClass: this.testClassArr[0] })
    }
    .height('100%')
    .width('100%')
  }
}

测试结果图

在这里插入图片描述

在上文的示例中,PropChild组件没有改变@Prop testClass: MyClass的值,所以这时较优的选择是使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择。

【正例】

@Observed
class MyClass6 {
  public num: number = 0;

  constructor(num: number) {
    this.num = num;
  }
}

@Component
struct PropChild6 {
  // @Prop testClass: MyClass6; // @Prop 装饰状态变量会深拷贝
  @ObjectLink testClass: MyClass6;  // @ObjectLink 装饰状态变量不会深拷贝

  build() {
    Text(`PropChild testNum ${this.testClass.num}`)
      .fontColor(Color.Black)
      .fontWeight(FontWeight.Bold)
      .fontSize(20)
  }
}

@Entry
@Component
struct TestStateMangager {
  @State message: string = '状态管理优秀实践';
  @State testClassArr: MyClass6[] = [new MyClass6(2)];

  build() {
    Column({ space: 10 }) {
      Text(this.message)
        .id('TestStateMangagerHelloWorld')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(`TestStateMangager testNum is ${this.testClassArr[0].num} `).fontColor(Color.Black)
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .onClick(() => {
          this.testClassArr[0].num += 1;
          console.log(`点击了 TestStateMangager testNum this.testClassArr[0].num = ${this.testClassArr[0].num}`)
        })
      // PropChild6没有改变@Prop testClassArr: MyClass6的值,所以这时最优的选择是使用@ObjectLink
      // PropChild6({ testClass: this.testClassArr[0] })

      // 当子组件不需要发生本地改变时,优先使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择
      PropChild6({ testClass: this.testClassArr[0] })
    }
    .height('100%')
    .width('100%')
  }
}

三、不使用状态变量强行更新非状态变量关联组件

【反例】

@Entry
@Component
struct TestStateMangager2 {
  @State message: string = '状态管理优秀实践';

  //不使用状态变量强行更新非状态变量关联组件

  @State needsUpdate: boolean = true;
  realStateArr: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器
  realState: Color = Color.Yellow;

  updateUIArr(param: Array<number>): Array<number> {
    const triggerAGet = this.needsUpdate;
    return param;
  }
  updateUI(param: Color): Color {
    const triggerAGet = this.needsUpdate;
    return param;
  }
  build() {
    Column({ space: 20 }) {
      ForEach(this.updateUIArr(this.realStateArr),
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("add item")
        .onClick(() => {
          // 改变realStateArr不会触发UI视图更新
          this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1);

          // 触发UI视图更新
          this.needsUpdate = !this.needsUpdate;
        })
      Text("chg color")
        .onClick(() => {
          // 改变realState不会触发UI视图更新
          this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow;

          // 触发UI视图更新
          this.needsUpdate = !this.needsUpdate;
        })
    }.backgroundColor(this.updateUI(this.realState))
    .width(200).height(500)
  }
}

效果图1

在这里插入图片描述

效果图2

在这里插入图片描述

上述示例存在以下问题:

  • 应用程序希望控制UI更新逻辑,但在ArkUI中,UI更新的逻辑应该是由框架来检测应用程序状态变量的更改去实现。
  • this.needsUpdate是一个自定义的UI状态变量,应该仅应用于其绑定的UI组件。变量this.realStateArr、this.realState没有被装饰,他们的变化将不会触发UI刷新。
  • 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realStateArr、this.realState的更新,此方法不合理且更新性能较差。

【正例】

要解决此问题,应将realStateArr和realState成员变量用@State装饰。一旦完成此操作,就不再需要变量needsUpdate。

@Entry
@Component
struct TestStateMangager3 {
  @State realStateArr: Array<number> = [4, 1, 3, 2];
  @State realState: Color = Color.Yellow;
  build() {
    Column({ space: 20 }) {
      ForEach(this.realStateArr,
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("add item")
        .onClick(() => {
          // 改变realStateArr触发UI视图更新
          this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1);
          console.log("点击了 add item")
        })
      Text("chg color")
        .onClick(() => {
          // 改变realState触发UI视图更新
          this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow;
          console.log("点击了 chg color")
        })
    }.backgroundColor(this.realState)
    .width(200).height(500)
  }
}

效果图1

在这里插入图片描述
效果图2

在这里插入图片描述

四、精准控制状态变量关联的组件数

建议每个状态变量关联的组件数应该少于20个。精准控制状态变量关联的组件数能减少不必要的组件刷新,提高组件的刷新效率。有时开发者会将同一个状态变量绑定多个同级组件的属性,当状态变量改变时,会让这些组件做出相同的改变,这有时会造成组件的不必要刷新,如果存在某些比较复杂的组件,则会大大影响整体的性能。但是如果将这个状态变量绑定在这些同级组件的父组件上,则可以减少需要刷新的组件数,从而提高刷新的性能。

【反例】

@Observed
class Translate {
  translateX: number = 20;
}

@Component
struct Title {
  @ObjectLink translateObj: Translate;

  build() {
    Row() {
      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
      Image($r('app.media.icon'))
        .width(50)
        .height(50)
        .translate({
          x: this.translateObj.translateX // this.translateObj.translateX 绑定在Image和Text组件上
        })
      Text("Title")
        .fontSize(20)
        .translate({
          x: this.translateObj.translateX
        })
    }
  }
}

@Entry
@Component
struct TestStateMangager4 {
  @State message: string = '精准控制状态变量关联的组件数';
  @State translateObj: Translate = new Translate();

  build() {
    Column() {
      Title({
        translateObj: this.translateObj
      })
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)
      .translate({
        x: this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上
      })

      Button("move")
        .translate({
          x: this.translateObj.translateX
        })
        .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
          })
        })
    }
  }
}

测试图

在这里插入图片描述

在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有的子组件其实都处于Page下的Column中,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。

【正例】

@Observed
class Translate {
  translateX: number = 20;
}

@Component
struct Title {
  @ObjectLink translateObj: Translate;

  build() {
    Row() {
      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
      Image($r('app.media.icon'))
        .width(50)
        .height(50)
        .translate({
          x: this.translateObj.translateX // this.translateObj.translateX 绑定在Image和Text组件上
        })
      Text("Title")
        .fontSize(20)
        .translate({
          x: this.translateObj.translateX
        })
    }
  }
}

@Component
struct Title2 {
  build() {
    Row() {
      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
      Image($r('app.media.icon'))
        .width(50)
        .height(50)
      Text("Title")
        .fontSize(20)
    }
  }
}

@Entry
@Component
struct TestStateMangager4 {
  @State message: string = '精准控制状态变量关联的组件数';
  @State translateObj: Translate = new Translate();

  build() {
    // 反例
    // Column() {
    //   Title({
    //     translateObj: this.translateObj
    //   })
    //   Stack() {
    //   }
    //   .backgroundColor("black")
    //   .width(200)
    //   .height(400)
    //   .translate({
    //     x: this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上
    //   })
    //
    //   Button("move")
    //     .translate({
    //       x: this.translateObj.translateX
    //     })
    //     .onClick(() => {
    //       animateTo({
    //         duration: 50
    //       }, () => {
    //         this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
    //       })
    //     })
    // }
    //正例
    Column() {
      Title2()
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)

      Button("move")
        .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
          })
        })
    }
    .translate({
      // 子组件Stack和Button设置了同一个translate属性,可以统一到Column上设置
      x: this.translateObj.translateX
    })
  }
}

测试效果图

在这里插入图片描述

五、合理控制对象类型状态变量关联的组件数量

如果将一个复杂对象定义为状态变量,需要合理控制其关联的组件数。当对象中某一个成员属性发生变化时,会导致该对象关联的所有组件刷新,尽管这些组件可能并没有直接使用到该改变的属性。为了避免这种“冗余刷新”对性能产生影响,建议合理拆分该复杂对象,控制对象关联的组件数量。具体可参考精准控制组件的更新范围状态管理合理使用开发指导 两篇文章。

六、查询状态变量关联的组件数

在应用开发中,可以通过HiDumper查看状态变量关联的组件数,进行性能优化。具体可参考状态变量组件定位工具实践

七、避免在for、while等循环逻辑中频繁读取状态变量

在应用开发中,应避免在循环逻辑中频繁读取状态变量,而是应该放在循环外面读取。

【反例】

@Entry
@Component
struct TestStateMangager5 {
  @State message: string = '';

  build() {
    //【反例】
    Column() {
      Button('点击打印日志')
        .onClick(() => {
          for (let i = 0; i < 10; i++) {
            this.message = "" + i;
            // hilog.info(0x0000, 'TAG', '%{public}s', this.message);
            console.log(`消息内容是: ${this.message}`)
          }
        })
        .width('90%')
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
      top: 15
    })
  }
}

测试结果图

在这里插入图片描述

【正例】

import hilog from '@ohos.hilog';

@Entry
@Component
struct Index {
  @State message: string = '';

  build() {
    Column() {
      Button('点击打印日志')
        .onClick(() => {
          let logMessage: string = this.message;
          for (let i = 0; i < 10; i++) {
            hilog.info(0x0000, 'TAG', '%{public}s', logMessage);
          }
        })
        .width('90%')
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
      top: 15
    })
  }
}

八、建议使用临时变量替换状态变量

在应用开发中,应尽量减少对状态变量的直接赋值,通过临时变量完成数据计算操作。

状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行依赖该状态变量的组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的行为,从而提高应用性能。状态变量行为可参考@State装饰器:组件内状态

【反例】

import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State message: string = '';

  appendMsg(newMsg: string) {
    // 性能打点
    hiTraceMeter.startTrace('StateVariable', 1);
    this.message += newMsg;
    this.message += ';';
    this.message += '<br/>';
    hiTraceMeter.finishTrace('StateVariable', 1);
  }

  build() {
    Column() {
      Button('点击打印日志')
        .onClick(() => {
          this.appendMsg('操作状态变量');
        })
        .width('90%')
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
      top: 15
    })
  }
}

【正例】

import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State message: string = '';

  appendMsg(newMsg: string) {
    // 性能打点
    hiTraceMeter.startTrace('TemporaryVariable', 2);
    let message = this.message;
    message += newMsg;
    message += ';';
    message += '<br/>';
    this.message = message;
    hiTraceMeter.finishTrace('TemporaryVariable', 2);
  }

  build() {
    Column() {
      Button('点击打印日志')
        .onClick(() => {
          this.appendMsg('操作临时变量');
        })
        .width('90%')
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
      top: 15
    })
  }
}

【总结】

计算方式耗时(局限不同设备和场景,数据仅供参考)说明
直接操作状态变量1.01ms增加了ArkUI不必要的查询和渲染行为,导致性能劣化
使用临时变量计算0.63ms减少了ArkUI不必要的行为,优化性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值