#我的鸿蒙开发手记#HarmonyOS应用开发性能优化(篇二) 原创

FelixLu
发布于 2025-5-15 19:22
浏览
0收藏

1. 尽量减少布局的嵌套层数

在进行页面布局开发时,去除冗余的布局嵌套,使用相对布局、绝对定位、自定义布局、Grid、GridRow等扁平化布局,减少布局的嵌套层数,避免系统绘制更多的布局组件,使用@Builder替换自定义组件减少嵌套层级,达到优化性能、减少内存占用的目的。

1.1. 删除无用的Stack/Column/Row嵌套

例如在Row容器包含一个同样也是Row容器的子级。这种嵌套实际是多余的,并且会给布局层次结构造成不必要的开销。

优化前:

Row() {
  Row() {
    Image()
    Text()
  }
  Image()
}

优化后:

Row() {
  Image()
  Text()
  Image()
}

1.2. 移除冗余节点

应该删除冗余的布局嵌套,例如非@Entry组件的build最外层的无用容器嵌套、无用的Stack或Column嵌套等,减少布局层数。

优化前:

@Component
struct ComponentA {
  build() {
    Column() {
      ComponentB()
    }
  }
}

@Component
struct ComponentB {
  build() {
    // 应该删除build最外层的无用容器嵌套
    Column() {
      Text("")
    }
  }
}

优化后:

@Component
struct ComponentA {
  build() {
    Column() {
      ComponentB()
    }
  }
}

@Component
struct ComponentB {
  build() {
    Text("")
  }
}

1.3. 使用系统高阶组件实现布局嵌套

使用系统提供的高阶组件可以更高效地实现复杂布局嵌套:

  • List组件不仅支持线性排布,还具备懒加载与滑动功能,适用于列表型内容展示。
  • Grid/GridItem组件提供网格布局能力,同样支持懒加载与滑动,适合宫格样式内容组织。
  • RelativeContainer为相对布局容器,可通过描述组件间的相对位置关系进行横向与纵向的二维布局控制,适用于复杂排版需求。

这些组件均具备较强的性能优化能力,推荐优先使用。

2. 合理管理状态变量

合理地使用状态变量,精准控制组件的更新范围,控制状态变量关联组件数量,控制对象级状态变量的成员变量关联组件数,高负载场景使用AttributeModifier替代@State,减少系统的组件渲染负载,提升应用流畅度。

3. 合理使用系统接口,避免冗余操作

合理使用系统的高频回调接口,删除不必要的Trace和日志打印,避免注册系统冗余回调,减少系统开销。

3.1. 优化状态管理,精准控制刷新范围

3.1.1. 控制对象级状态变量成员关联的组件数量,减小刷新范围

滑动场景下,应该控制状态变量关联的组件数量,如果一个状态关联过多的组件,当这个变量更新时会引起过多的组件重新绘制渲染,建议关联数量限制在20个以内。合理控制状态更新范围,避免关联刷新较大范围或者渲染较慢的组件。

  • 不推荐

class Info {
  name: string = ""
  userDefine: string = ""
  size: number = 0
  image: Resource | undefined = undefined
}

@Entry
@Component
struct Index {
  @State userInfo: Info = new Info()

  build() {
    Row() {
      Column() {
        // Image和Text等信息放在一个userInfo对象中
        Image(this.userInfo.image)
          .width(50)
          .height(50)
        Text(this.userInfo.userDefine)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Text(this.userInfo.size.toString())
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Text(this.userInfo.name)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
     .width('100%')
    }
   .height('100%')
  }
}
  • 推荐

class Info {
  name: string = ""
  userDefine: string = ""
  size: number = 0
}

@Entry
@Component
struct Index {
  @State userInfo: Info = new Info()
  @State image: Resource | undefined = undefined

  build() {
    Row() {
      Column() {
        // Image单独绑定一个状态变量,避免只更新text时也触发Image更新
        Image(this.image)
          .width(50)
          .height(50)
        Text(this.userInfo.userDefine)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Text(this.userInfo.size.toString())
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Text(this.userInfo.name)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
     .width('100%')
    }
   .height('100%')
  }
}

3.1.2. 合理拆分对象,避免过度设计

过分追求状态结构拆分可能在某些场景导致组件设计过度,不利于维护。此时,可以将对象或类上经常一起改变的几个属性聚合成一个新的对象或类模型,并使用@Observed装饰器修饰,再作为属性挂载到之前的对象或类上。通过此方法,当属性变化时ArkUI只会通知变化给新的对象或类,不会通知最上层的对象。这样既可以有效的减少无用渲染次数,又能使代码更好维护。

如类ClassA上存在属性b、c、d。其中c和d经常一起发生变化,即当c的状态修改时同时也要修改d的状态。

class ClassA {
  b: string
  c: number
  d: boolean
}

此时,将c和d组合在一起做为新的类ClassE的属性并使用@Observed装饰器修饰。对于ClassA去掉c、d属性,新增属性e且其类型为ClassE,设计如下:

class ClassA {
  b: string
  e: ClassE
}

@Observed
class ClassE {
  c: number
  d: boolean
}

当ClassA实例的属性e中的属性c的值变化时,状态变化会通知使用ClassE实例的组件重新渲染,不会通知关联ClassA实例的组件更新,即只使用了ClassA实例b属性的组件不会重新渲染。

3.1.3. 合理使用状态变量,避免冗余损耗

场景:使用一个状态变量flag控制两个组件,其中一个组件需要播放渐变动画,另一个组件不播放动画直接显示或隐藏。

  • 不推荐的实现

@Component
struct Index {
  @State flag: boolean = false

  build() {
    Column({ space: 24 }) {
      Button("changeStatus").onClick((event: ClickEvent) => {
        animateTo({ duration: 600 }, () => {
          this.flag =!this.flag
        })
      })

      Image($r('app.media.startIcon'))
       .width(56)
       .aspectRatio(1)
        // 使用一个flag导致冗余的动画animation设置,
        // 应该设置一个新的状态变量@State flag2单独控制该属性的刷新
       .visibility(this.flag? Visibility.Visible : Visibility.None)
       .animation({ duration: 0 })

      Row().width(100).aspectRatio(1).backgroundColor(this.flag? '#000' : '#ff0')
    }
  }
}
  • 推荐的实现

@Component
struct Index {
  @State flag: boolean = false
  @State flag2: boolean = false

  build() {
    Column({ space: 24 }) {
      Button("changeStatus").onClick((event: ClickEvent) => {
        animateTo({ duration: 600 }, () => {
          this.flag =!this.flag
        })
      })

      Image($r('app.media.startIcon'))
       .width(56)
       .aspectRatio(1)
       .visibility(this.flag2? Visibility.Visible : Visibility.None)

      Row().width(100).aspectRatio(1).backgroundColor(this.flag? '#000' : '#ff0')
    }
  }
}

3.2. 精准控制组件的更新范围

3.2.1. 减少不必要的参数层次传递

@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink三种方案的实现方式是逐级向下传递状态,当共享状态的组件间层级相差较大时,会出现状态层层传递的现象。对于没有使用该状态的中间组件而言,这是“额外的消耗”。因此,对于跨越多层的状态变量传递,使用@Provide+@Consume方案更为合理。

同时,也要注意不要滥用@Provide+@Consume,应优先选择共享范围能力小的装饰器方案,减少不同模块间的数据耦合,便于状态及时回收。建议选择装饰器的优先级为:@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。

3.2.2. 使用@Link/@ObjectLink替代@Prop

@Prop是深拷贝,@Link/@ObjectLink是引用传递。所以在@Prop和@ObjectLink使用效果相同的场景下,优先使用@ObjectLink的方式加快对象创建速度,减少系统内存开销。

3.2.3. 使用DisplaySync精准控制每一帧的组件刷新范围

在某些高负载场景中,可以使用DisplaySync控制每一帧的刷新范围,均衡负载,从而减低丢帧率。

aboutToAppear(): void {
  // 创建DisplaySync对象
  this.displaySync = displaySync.create()
  // 初始化期望帧率
  let range: ExpectedFrameRateRange = {
    expected: 120,
    min: 60,
    max: 120
  }
  // 设置期望帧率
  this.displaySync.setExpectedFrameRateRange(range)
  // 设置帧回调监听
  this.displaySync.on("frame", () => {
   // ...
  })
  // 开启监听帧回调
  this.displaySync.start()
 // ...
}

3.3. 避免不必要的创建和读取状态变量

3.3.1. 删除冗余的状态变量标记

状态变量的管理有一定的开销,应在合理场景使用,普通的变量用状态变量标记可能会导致性能劣化。

  • 反例

@Component
struct component {
  @State bgcolor: string | Color = '#ffffffff'
  @State selectColor: string | Color = '#007DFFF'

  build() {
  }
}
  • 正例

@Component
struct component {
  bgcolor: string | Color = '#ffffffff'
  selectColor: string | Color = '#007DFFF'

  build() {
  }
}

3.3.2. 避免在For/while等循环函数中重复读取状态变量

状态变量的读取耗时远大于普通变量的读取耗时,因此要避免重复读取状态变量,而是应该放在循环外面读取,例如在打印For/while循环中打印状态变量的日志信息。

  • 反例

@Component
struct Page {
  @State message: string = ""

  build() {
    Column() {
      Button('点击打印日志')
       .onClick(() => {
          for (let i = 0; i < 10; i++) {
            console.debug(this.message)
          }
        })
    }
  }
}
  • 正例

@Component
struct Page {
  @State message: string = ""

  build() {
    Column() {
      Button('点击打印日志')
       .onClick(() => {
          let logMessage: string = this.message
          for (let i = 0; i < 10; i++) {
            console.debug(logMessage)
          }
        })
    }
  }
}

3.4. 避免在系统高频回调用进行冗余和耗时操作

为了提升系统性能,应避免在高频回调函数中执行冗余或耗时操作。例如:onTouch、onItemDragMove、onDragMove、onScroll、onMouse、onVisibleAreaChange、onAreaChange、onActionUpdate、animator的onFrame回调,以及组件复用和生命周期相关的aboutToReuse、aboutToAppear、aboutToDisappear等。这些回调通常在每一帧渲染时都会被触发,若处理不当,会显著增加系统负载,导致卡顿或掉帧,影响应用整体流畅性。

  • 示例一

// 反例
Scroll() {
  ForEach(this.arr, (item: number) => {
    Text("ListItem" + item)
  }, (item: number) => item.toString())
}
.width("100%")
.height("100%")
.onScroll(() => {
  hiTraceMeter.startTrace('ScrollSlide', 1002)
  // 业务逻辑
  //...
  hiTraceMeter.finishTrace('ScrollSlide', 1002)
})

// 正例
Scroll() {
  ForEach(this.arr, (item: number) => {
    Text("ListItem" + item)
  }, (item: number) => item.toString())
}
.width("100%")
.height("100%")
.onScroll(() => {
  // 业务逻辑
  //...
})
  • 示例二

// 反例
Scroll() {
  ForEach(this.arr, (item: number) => {
    Text("ListItem" + item)
  }, (item: number) => item.toString())
}
.width("100%")
.height("100%")
.onScroll(() => {
  hilog.info(1002, 'Scroll', 'ListItem')
  // 业务逻辑
  //...
})

// 正例
Scroll() {
  ForEach(this.arr, (item: number) => {
    Text("ListItem" + item)
  }, (item: number) => item.toString())
}
.width("100%")
.height("100%")
.onScroll(() => {
  // 业务逻辑
  //...
})

3.5. 删除冗余Trace和日志打印

Trace和日志打印会比较消耗系统性能,因此我们应该避免冗余的Trace和日志打印。推荐在Release版本中,尽量删除所有Trace信息,删除Debug日志,减少额外的系统开销。在Release版本中,debug日志的入参字符串的拼接也会有开销,特别是拼接状态变量的情况下。

  • 删除冗余trace

//反例代码如下:
@Component
struct ComponentA {
  build() {
  }

  aboutToAppear(): void {
    hiTraceMeter.startTrace('HITRACE_TAG_APP', 1001)
    // 业务代码
    //...
    hiTraceMeter.finishTrace('HITRACE_TAG_APP', 1001)
  }
}

// 正例代码如下:
@Component
struct ComponentB {
  build() {
  }

  aboutToAppear(): void {
    // 业务代码
    //...
  }
}
  • Release版本删除debug日志

//反例代码如下:
@Component
struct MyComponentA {
  @State text: string = ""
  build() {
  }

  aboutToAppear(): void {
    hilog.debug(0xFF00, 'textTug', 'ComponentA_aboutToAppear_start' + this.text)
    // 业务代码
    //...
    hilog.debug(0xFF00, 'textTug', 'ComponentA_aboutToAppear_end' + this.text)
  }
}

// 正例代码如下:
@Component
struct MyComponentB {
  @State text: string = ""
  build() {
  }

  aboutToAppear(): void {
    // 业务代码
    //...
  }
}

3.6. 避免设置冗余系统回调监听

冗余的系统回调监听,会额外消耗系统开销去做计算和函数回调消耗。比如设置了onAreaChange,就算回调中没有任何逻辑,系统也会在C++侧去计算该组件的大小和位置变化情况,并且把结果回调到TS侧,额外消耗了系统开销。

  • 反例

//反例代码如下:
@Component
struct NegativeOfOnClick {
  build() {
    Button('Click', { type: ButtonType.Normal, stateEffect: true })
     .onClick(() => {
        hiTraceMeter.startTrace('ButtonClick', 1004)
        hilog.info(1004, 'Click', 'ButtonType.Normal')
        hiTraceMeter.finishTrace('ButtonClick', 1004)
        // 业务代码
        //...
      })
     .onAreaChange((oldValue: Area, newValue: Area) => {
        // 无任何代码
      })
  }
}
  • 正例

//正例代码如下:
@Component
struct PositiveOfOnClick {
  build() {
    Button('Click', { type: ButtonType.Normal, stateEffect: true })
     .onClick(() => {
        // 业务代码
        //...
      })
  }
}

3.7. 避免在ResourceManager的getXXXSync接口入参中直接使用资源信息

避免在ResourceManager的getXXXSync接口入参中直接使用资源信息,推荐使用资源id作为入参,例如推荐用法为:resourceManager.getStringSync($r('app.string.test').id)

  • 反例

// 反例
aboutToAppear(): void {
  hiTraceMeter.startTrace('getStringSync', 1)
  // getStringSync接口的入参直接使用资源,未使用资源ID
  getContext().resourceManager.getStringSync($r('app.string.test'))
  hiTraceMeter.finishTrace('getStringSync', 1)
}
  • 正例

// 正例:
aboutToAppear(): void {
  hiTraceMeter.startTrace('getStringSyncAfter', 2)
  // getStringSync接口的入参使用了资源ID
  getContext().resourceManager.getStringSync($r('app.string.test').id)
  hiTraceMeter.finishTrace('getStringSyncAfter', 2)
}


--未完待续--


学习更多鸿蒙应用开发技能,请观看视频教程:

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-5-16 20:24:48修改
收藏
回复
举报
回复
    相关推荐