HarmonyOS应用性能优化实战-组件复用四板斧

概述

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

组件复用四板斧:

  • 第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。

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

  • 第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。

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

组件复用原理机制

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

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

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

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

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

反例:

@Entry
@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
@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() {
    // 此处使用`${}`来进行按引用传递,让@Builder感知到数据变化,进行UI刷新
    Text(子组件 + ${$$.paramA})
      .fontSize(30)
      .fontWeight(30)
  }
}

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

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

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

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

    反例:

    @Entry
    @Component
    struct PreciseRefreshing {
      @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/@ObjectLink替代@Prop减少深拷贝,提升组件创建速度

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

    反例:

    @Component
    struct ChildComponent {
      @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来进行,每个@Prop装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。

    正例:

    @Component
    struct ChildComponent {
      @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来进行,子组件@Link包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。

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

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

    反例:

    @Entry
    @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
    @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
    @Component
    struct ChildComponent {
      @State desc: string = '';
      @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
    @Component
    struct ChildComponent {
      @State desc: string = '';
      @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来进行。 

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值