首页 > 其他分享 >鸿蒙(HarmonyOS)应用性能优化实战-组件复用四板斧

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

时间:2024-06-07 21:59:15浏览次数:18  
标签:index 鸿蒙 item 复用 HarmonyOS fontSize 组件 chatInfo

概述

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

组件复用四板斧:

  • 第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的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来进行。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

标签:index,鸿蒙,item,复用,HarmonyOS,fontSize,组件,chatInfo
From: https://blog.csdn.net/m0_70748458/article/details/139525828

相关文章

  • 鸿蒙 App 应用开发性能优化全面指南
    优化应用性能对于应用开发至关重要。通过高性能编程、减少丢帧卡顿、提升应用启动和响应速度,可以有效提升用户体验。本文将介绍一些优化应用性能的方法,以及常用的性能调优工具。ArkTS高性能编程为了提升代码执行速度,进而提升应用整体性能,可以采取以下措施:使用ArkTS高性......
  • 【纯血鸿蒙】——如何实现多端部署?(开发重点建议收藏)
    一次开发,多端部署介绍鸿蒙系统采用微内核分布式系统的架构,分布式技术逐渐打破单一硬件边界,一个应用或服务,可以在不同的硬件设备之间随意调用、互助共享,让用户享受无缝的全场景体验。而作为应用开发者,广泛的设备类型也能为应用带来广大的潜在用户群体。但是如果一个应用需要在......
  • IO多路复用
    基本概念IO多路复用指用一个线程来处理多个客户端请求Epoll是一种IO事件通知机制IO输入输出对象可以是文件,网络,管道等用文件描述符fd表示的事件Event分为可读事件和可写事件,有两种触发机制(水平触发和边缘触发)水平触发机制:只要fd关联的内核缓冲区非空/非满,数据就可以一直读/......
  • 鸿蒙HarmonyOS实战-ArkTS语言基础类库(通知)
    ......
  • 【最新鸿蒙应用开发】——沙箱机制是什么?作用?场景?
    沙箱机制1.什么是沙箱机制?1.1.概念在操作系统当中,沙箱机制(Sandboxing)是一种安全机制,用于限制程序代码的访问权限,防止恶意软件对系统造成破坏。在沙箱环境中,程序只能访问特定的资源,如文件、注册表、网络等,而不能访问其他系统资源。这样,即使恶意软件试图执行有害操作,也会因......
  • WebviewController进行混合开发,鸿蒙星河版API(11)
    @ohos.web.webview提供web控制能力,web组件提供网页显示的能力,同时也可以执行网页中定义的JS方法。一、第一步创建WebviewController实例controller:WebviewController=newwebview.WebviewController()二、web组件加载html文件build(){Navigation(){Col......
  • 通过端口复用直接进行正向tcp代理--win会被识别为病毒
    学习项目,win会被识别为病毒,关闭病毒和威胁防护,参考https://blog.csdn.net/u_say2what/article/details/134669122main.gopackagemainimport( "context" "fmt" _"golang.org/x/sys/unix" "golang.org/x/sys/windows" "io" "......
  • HarmonyOS(二十三)——HTTP请求实战一个新闻列表
    在前一篇文章,我们已经知道如何实现一个http请求的完整流程,今天就用官方列子实战一个简单的新闻列表。进一步掌握ArkTS的声明式开发范式,数据请求,常用系统组件以及touch事件的使用。主要包含以下功能:数据请求。列表下拉刷新。列表上拉加载。看一下最终的效果。1.实战分析......
  • 鸿蒙HarmonyOS实战-ArkTS语言基础类库(容器类库)
    ......
  • 纯血鸿蒙和传统安卓的区别?优势?
    鸿蒙操作系统一、基于微内核架构的操作系统。什么是微内核设计?微内核设计是一种操作系统设计方法,它将操作系统的核心功能模块化,将尽可能多的功能移到用户空间,只在内核中保留最基本的功能,如进程管理和内存管理。微内核设计有助于提高系统的灵活性、可靠性和安全性,因为内核中的功......