HarmonyOS 鸿蒙Next 组件复用

发布于 1周前 作者 eggper 最后一次编辑是 5天前 来自 鸿蒙OS

【性能优化】HarmonyOS 鸿蒙Next 组件复用

HarmonyOS Next应用开发案例(持续更新中……)

HarmonyOS Next性能指导总览

本篇文章链接,请访问:https://gitee.com/harmonyos-cases/cases/blob/master/docs/performance/component_recycle_case.md

概述

在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过组件复用四板斧提升复用性能。

组件复用四板斧:

  • 第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
  • 第二板斧,优化状态管理,精准控制组件刷新范围,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。
  • 第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。
  • 第四板斧,不要使用函数/方法作为复用组件的入参,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。

组件复用原理机制

  1. 如上图①中,ListItem N-1滑出可视区域即将销毁时,如果标记了[@Reusable](/user/Reusable),就会进入这个自定义组件所在父组件的复用缓存区。需注意在自定义组件首次显示时,不会触发组件复用。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其是该复用组件具有相同的布局结构,仅有某些数据差异时,通过组件复用可以提高列表页面的加载速度和响应速度。

  2. 如上图②中,复用缓存池是一个Map套Array的数据结构,以reuseId为key,具有相同reuseId的组件在同一个Array中。如未设置reuseId,则reuseId默认是自定义组件的名字。

  3. 如上图③中,发生复用行为时,会自动递归调用复用池中取出的自定义组件的aboutToReuse回调,应用可以在这个时候刷新数据。

第一板斧,减少组件复用的嵌套层级

在组件复用场景下,过深的自定义组件的嵌套会增加组件复用的使用难度,比如需要逐个实现所有嵌套组件中aboutToReuse回调实现数据更新;因此推荐优先使用[@Builder](/user/Builder)替代自定义组件,减少嵌套层级,利于维护切能提升页面加载速度。正反例如下:

反例:

[@Entry](/user/Entry)
[@Component](/user/Component)
struct ReduceLevel {
  private data: BasicDateSource = new BasicDateSource();

aboutToAppear(): void { for (let index = 0; index < 30; index++) { this.data.pushData(index.toString()) } }

build() { Column() { List() { LazyForEach(this.data, (item: string) => { ListItem() { //反例 使用自定义组件 ComponentA({ desc: item }) } }, (item: string) => item) } } } }

@Reusable @Component struct ComponentA { @State desc: string = ‘’;

aboutToReuse(params: ESObject): void { this.desc = params.desc as string; }

build() { // 在复用组件中嵌套使用自定义组件 ComponentB({ desc: this.desc }) } }

@Component struct ComponentB { @State desc: string = ‘’; // 嵌套的组件中也需要实现aboutToReuse来进行UI的刷新 aboutToReuse(params: ESObject): void { this.desc = params.desc as string; }

build() { Column() { Text(‘子组件’ + this.desc) .fontSize(30) .fontWeight(30) } } }

上述反例的操作中,在复用的自定义组件中嵌套了新的自定义组件。ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和和RenderNod渲染e的耗时。且嵌套的自定义组件ComponentB,也需要实现aboutToReuse来进行数据的刷新。

正例:

[@Entry](/user/Entry)
[@Component](/user/Component)
struct ReduceLevel {
  private data: BasicDateSource = new BasicDateSource();

aboutToAppear(): void { for (let index = 0; index < 30; index++) { this.data.pushData(index.toString()) } }

build() { Column() { List() { LazyForEach(this.data, (item: string) => { ListItem() { // 正例 ChildComponent({ desc: item }) } }, (item: string) => item) } } } }

// 正例 使用组件复用 @Reusable @Component struct ChildComponent { @State desc: string = ‘’;

aboutToReuse(params: Record<string, Object>): void { this.desc = params.desc as string; }

build() { Column() { // 使用@Builder,可以减少自定义组件创建和渲染的耗时 ChildComponentBuilder({ paramA: this.desc }) } } }

class Temp { paramA: string = ‘’; }

@Builder function ChildComponentBuilder($$: Temp) { Column() { // 此处使用<span class="hljs-title">$</span>{}来进行按引用传递,让@Builder感知到数据变化,进行UI刷新 Text(子组件 + ${$$.paramA}) .fontSize(30) .fontWeight(30) } }

上述正例的操作中,在复用的自定义组件中用[@Builder](/user/Builder)来代替了自定义组件。避免了CustomNode节点创建和RenderNode渲染的耗时。

第二板斧,优化状态管理,精准控制组件刷新范围使用

1.使用attributeModifier精准控制组件属性的刷新,避免组件不必要的属性刷新

复用场景常用在高频的刷新场景,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。正反例如下:

反例:

[@Entry](/user/Entry)
[@Component](/user/Component)
struct PreciseRefreshing {
  [@State](/user/State) mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表

build() { Column() { List() { LazyForEach(this.mainContentData, (item: VideoDataType) => { ListItem() { MyComponent({ authorName: item.authorName, fontSize: item.fontWeight }) } }, (item: VideoDataType) => item.desc + item.fontWeight) } } } }

@Reusable @Component export struct MyComponent { … @State fontSize: number = 0;

aboutToReuse(params: ESObject): void { this.authorName = params.authorName; this.fontSize = params.fontSize; }

build() { RelativeContainer() { Text(this.videoDesc) .textAlign(TextAlign.Center) .fontStyle(FontStyle.Normal) .fontColor(Color.Pink) .id(‘videoName’) .margin({ left: 10 }) .fontWeight(30) .alignRules({ ‘top’: { ‘anchor’: container, ‘align’: VerticalAlign.Top }, ‘left’: { ‘anchor’: ‘image’, ‘align’: HorizontalAlign.End } }) // 此处使用属性直接进行刷新,会造成Text所有属性都刷新 .fontSize(this.fontSize) } .width(‘100%’) .height(100) } }

上述反例的操作中,通过aboutToReuse对fontSize状态变量更新,进而导致组件的全部属性进行刷新,造成不必要的耗时。可以考虑对需要更新的组件的属性,进行精准刷新,避免不必要的重绘和渲染。

正例:

export class MyTextModifier implements AttributeModifier<TextAttribute> {
  private fontSize: number = 30;

constructor() { }

setFontSize(instance: TextAttribute,fontSize: number) { instance.fontSize = fontSize; return this; }

applyNormalAttribute(instance: TextAttribute): void { instance.textAlign(TextAlign.Center) instance.fontStyle(FontStyle.Normal) instance.fontColor(Color.Pink) instance.id(‘videoName’) instance.margin({ left: 10 }) instance.fontWeight(30) instance.fontSize(10) instance.alignRules({ ‘top’: { ‘anchor’: container, ‘align’: VerticalAlign.Top }, ‘left’: { ‘anchor’: ‘image’, ‘align’: HorizontalAlign.End } }) } }

@Entry @Component struct PreciseRefreshing { @State mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表

build() { Column() { List() { LazyForEach(this.mainContentData, (item: VideoDataType) => { ListItem() { MyComponent({… fontSize: item.fontWeight }) } }, (item: VideoDataType) => item.desc + item.fontWeight) } } } }

@Reusable @Component export struct MyComponent { … @State fontSize: number = 0; textModifier:MyTextModifier=new MyTextModifier();

aboutToReuse(params: ESObject): void { … this.fontSize = params.fontSize; this.textModifier.setFontSize(this.textModifier,this.fontSize) }

build() { RelativeContainer() { … Text(this.videoDesc) // 采用attributeModifier来对需要更新的fontSize属性进行精准刷新,避免不必要的属性刷新。 .attributeModifier(this.textModifier) … } } }

上述正例的操作中,通过attributeModifier属性来对text组件需要刷新的fontSize属性进行精准刷新,避免text其它不需要更改的属性的刷新。

2.使用[@Link](/user/Link)/[@ObjectLink](/user/ObjectLink)替代[@Prop](/user/Prop)减少深拷贝,提升组件创建速度

在父子组件数据同步时,如果仅仅是需要父组件向子组件同步数据,不存在修改子组件的数据变化不同步给父组件的需求。建议使用[@Link](/user/Link)/[@ObjectLink](/user/ObjectLink)替代[@Prop](/user/Prop),[@Prop](/user/Prop)在装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。正反例如下:

反例:

[@Component](/user/Component)
struct ChildComponent {
  [@Prop](/user/Prop) message: string;

build() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) } } }

@Entry @Component struct FatherComponent { @State message: string = ‘Hello World’;

build() { Column() { ChildComponent({ message: this.message }) } } }

上述反例的操作中,父子组件之间的数据同步用了[@Prop](/user/Prop)来进行,每个[@Prop](/user/Prop)装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。

正例:

[@Component](/user/Component)
struct ChildComponent {
  [@Link](/user/Link) message: string;

build() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) } } }

@Entry @Component struct FatherComponent { @State message: string = ‘Hello World’;

build() { Column() { ChildComponent({ message: this.message }) } .width(‘100%’) .height(‘100%’) } }

上述正例的操作中,父子组件之间的数据同步用了[@Link](/user/Link)来进行,子组件[@Link](/user/Link)包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。

第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成

在自定义组件复用的场景中,如果使用if/else条件语句来控制布局的结构,会导致在不同逻辑创建不同布局结构嵌套的组件,从而造成组件树结构的不同。此时我们应该使用reuseId来区分不同结构的组件,确保系统能够根据reuseId缓存各种结构的组件,提升复用性能。正反例如下:

反例:

[@Entry](/user/Entry)
[@Component](/user/Component)
struct ReuseID {
  ...
  build() {
    Column() {
      List({ scroller: this.scroller }) {
        LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
          ListItem() {
            Button({ type: ButtonType.Normal }) {
              Row() {
                if (chatInfo['isPublicChat']) {
                  PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
                } else {
                  ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
                    .onClick(() => {
                      const sessionType = (chatInfo as ChatSessionEntity).sessionType
                      autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
                      imLogic.chat.chatSort()
                    })
                }
              }.padding({ left: 16, right: 16 })
            }
            .type(ButtonType.Normal)
            .width('100%')
            .height('100%')
            .backgroundColor('#fff')
            .borderRadius(0)
          }
          .height(72)
          .swipeAction({
            end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
          })
        }, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
        )
      }
      .cachedCount(3)
      .backgroundColor('#fff')
      .onScrollIndex(startIndex => {
        this.listStartIndex = startIndex;
      })
      .width('100%')
      .height('100%')
    }
  }
}
@Reusable
@Component
struct PublicChatItem {
  ...
  aboutToReuse(params: ESObject): void {
    this.chatInfo = params.chatInfo
  }
  build() {
    ...
  }
}

@Reusable @Component struct ChatItem { aboutToReuse(params: ESObject): void { this.chatInfo = params.chatInfo } build() { … } }

上述反例的操作中,通过if else来控制组件树走不同的分支,分别复用PublicChatItem组件和ChatItem组件。导致更新if分支时仍然走删除重创的逻辑。考虑采用根据不同的分支设置不同的reuseId来提高复用的性能。

正例:

[@Entry](/user/Entry)
[@Component](/user/Component)
struct ReuseID {
  ...
  build() {
    Column() {
      List({ scroller: this.scroller }) {
        LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
          ListItem() {
            // 使用reuseId进行组件复用的控制
            InnerRecentChat({ chatInfo: chatInfo }).reuseId(this.lazyChatList.getReuseIdByIndex(index))
          }
          .height(72)
          .swipeAction({
            end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
          })
        }, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
        )
      }
      .cachedCount(3)
      .backgroundColor('#fff')
      .onScrollIndex(startIndex => {
        this.listStartIndex = startIndex;
      })
      .width('100%')
      .height('100%')
    }
  }
}

@Reusable @Component struct InnerRecentChat { … aboutToReuse(params: ESObject): void { this.chatInfo = params.chatInfo }

build() { Button({ type: ButtonType.Normal }) { Row() { if (this.chatInfo[‘isPublicChat’]) { PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat }) } else { ChatItem({ chatInfo: chatInfo as ChatSessionEntity }) .onClick(() => { const sessionType = (chatInfo as ChatSessionEntity).sessionType autoOpenChat({ sessionId: chatInfo.sessionId, sessionType }) imLogic.chat.chatSort() }) } }.padding({ left: 16, right: 16 }) } .type(ButtonType.Normal) .width(‘100%’) .height(‘100%’) .backgroundColor(’#fff’) .borderRadius(0) } }

class MtDataSource extends BasicDataSource{ private chatList:Array<ChatSessionEntity|IChat.PublicChat>=[]; private reuseIds:Array<string>=[];

public totalCount():number{ return this.chatList.length; }

public set (list:Array<ChatSessionEntity|IChat.PublicChat>){ this.chatList=list; this.reuseIds=list.map((value:ChatSessionEntity|IChat.PublicChat)=>{ if (value[‘isPublicChat’]) { return “public”; } else { if ((value as ChatSessionEntity).target?.isEmployeeEntity()) { return “employee” }else { return “group” } } }) this.notifyDataReload(); } pubilc getReuseIdByIndex(index:number):string{ return this.reuseIds } }

上述正例的操作中,通过reuseId来标识需要复用的组件,省去走if else删除重创的逻辑,提高组件复用的效率和性能。

第四板斧,避免使用函数/方法作为复用组件创建时的入参

由于在组件复用的场景下,每次复用都需要重新创建组件关联的数据对象,导致重复执行入参中的函数来获取入参结果。如果函数中存在耗时操作,会严重影响性能。正反例如下:

【反例】

// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
[@Reusable](/user/Reusable)
[@Component](/user/Component)
struct ChildComponent {
  [@State](/user/State) desc: string = '';
  [@State](/user/State) sum: number = 0;

aboutToReuse(params: Record<string, Object>): void { this.desc = params.desc as string; this.sum = params.sum as number; }

build() { Column() { Text(‘子组件’ + this.desc) .fontSize(30) .fontWeight(30) Text(‘结果’ + this.sum) .fontSize(30) .fontWeight(30) } } }

@Entry @Component struct Reuse { private data: BasicDateSource = new BasicDateSource();

aboutToAppear(): void { for (let index = 0; index < 20; index++) { this.data.pushData(index.toString()) } }

// 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作 count(): number { let temp: number = 0; for (let index = 0; index < 10000; index++) { temp += index; } return temp; }

build() { Column() { List() { LazyForEach(this.data, (item: string) => { ListItem() { // 此处sum参数是函数获取的,实际开发场景无法预料该函数可能出现的耗时操作,每次进行组件复用都会重复触发此函数的调用 ChildComponent({ desc: item, sum: this.count() }) } .width(‘100%’) .height(100) }, (item: string) => item) } } } }

上述反例的操作中,复用的子组件参数sum是通过耗时函数生成。该函数在每次组件复用时都需要执行,会造成性能问题,甚至是列表滑动过程中的卡顿丢帧现象。

【正例】

// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
[@Reusable](/user/Reusable)
[@Component](/user/Component)
struct ChildComponent {
  [@State](/user/State) desc: string = '';
  [@State](/user/State) sum: number = 0;

aboutToReuse(params: Record<string, Object>): void { this.desc = params.desc as string; this.sum = params.sum as number; }

build() { Column() { Text(‘子组件’ + this.desc) .fontSize(30) .fontWeight(30) Text(‘结果’ + this.sum) .fontSize(30) .fontWeight(30) } } }

@Entry @Component struct Reuse { private data: BasicDateSource = new BasicDateSource(); @State sum: number = 0;

aboutToAppear(): void { for (let index = 0; index < 20; index++) { this.data.pushData(index.toString()) } // 执行该异步函数 this.count(); }

// 模拟耗时操作逻辑 async count() { let temp: number = 0; for (let index = 0; index < 10000; index++) { temp += index; } // 将结果放入状态变量中 this.sum = temp; }

build() { Column() { List() { LazyForEach(this.data, (item: string) => { ListItem() { // 子组件的传参通过状态变量进行 ChildComponent({ desc: item, sum: this.sum }) } .width(‘100%’) .height(100) }, (item: string) => item) } } } }

上述正例的操作中,通过耗时函数count生成的结果不变,可以将其放到页面初始渲染时执行一次,将结果赋值给this.sum。在复用组件的参数传递时,通过this.sum来进行。

1 回复

作为IT专家,对于HarmonyOS鸿蒙Next组件复用四板斧的理解如下:

第一板斧:减少组件复用的嵌套层级。过深的嵌套会增加复用的难度和耗时,推荐优先使用@Builder替代自定义组件,以减少嵌套层级,提升页面加载速度。

第二板斧:优化状态管理。在复用的场景下,需要精准控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。

第三板斧:使用reuseId标记不同结构的组件构成。复用组件嵌套结构会变更的场景,应使用reuseId标记不同结构的组件,以提升复用性能。

第四板斧:避免使用函数/方法作为复用组件的入参。复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。

这四板斧是HarmonyOS鸿蒙Next系统中提升组件复用性能的关键方法。通过减少嵌套层级、优化状态管理、使用reuseId标记组件以及避免使用函数/方法作为入参,可以显著提升应用的性能,特别是在列表滚动、动态布局更新和地图渲染等场景下。

请注意,正确理解和应用这四板斧需要一定的专业知识和实践经验。如果在实施过程中遇到问题,建议详细查阅相关文档或参考示例代码。如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部