【HarmonyOS】性能优化——组件的封装与复用

1.组件动态创建

动态创建组件指的是在build生命周期前进行组件的创建。

这样做不仅可以节省组件创建的时间,还可以将独立的逻辑进行封装,有助于应用模块化开发。

1.1预创建原理

在声明式开发中,组件只能在build环节中被创建,无法在其他生命周期中被创建。ArkUI框架提供的自定义节点能力可以实现组件的预加载,预加载的组件可以进行属性和布局的设置。之后在页面加载时使用

1.2使用FrameNode自定义节点在动态布局下的优势

1.2.1减少自定义组件创建开销

在使用声明式开发范式中,使用ArkUI的自定义组件对节点树中的每个节点进行定义的效率不高

因为每个节点在ArkTS引擎中都需要分配内存空间来存储应用程序的自定义组件和状态变量,在节点的创建中,还必须执行组件ID、组件闭包、状态变量之间依赖关系的收集

使用FrameNode,则可以避免创建自定义组件对象和状态变量对象,无需进行依赖收集,提升组件的创建速度。

使用@Component创建自定义自定义组件实际上就是创建了一个自定义组件对象,对象实例中包括了@State、@Link等状态变量;同时UI框架还要执行依赖收集,即分析这个组件对象内部的状态变量之间、状态变量与UI之间的关系,以建立一套响应式更新机制,比较耗时

1.2.2可以直接操作组件树

声明式开发范式无法直接调整组件实例间的结构关系,而使用FrameNode可以很方便的操控组件树,将一个节点移植到另一个地方

1.3动态添加、更新、删除组件

1.3.1动态添加

  1. 创建自定义节点(使用@Builder)
typescript 复制代码
@Builder
function ItemView(num: number){
  Column() {
    Text(`这是第${num}个组件`)
  }.width('100%').height(50)
}
  1. 实现NodeController,用于自定义节点的创建、显示、更新等操作的管理,并负责将自定义节点挂载至NodeContainer上
typescript 复制代码
class Params{
  num: number 
  constructor(num: number) {
    this.num = num;
  }
}
class ItemNodeControl extends NodeController{
  private itemNode: BuilderNode<[Params]> | null = null;
  private num: number = 0;
  
  constructor(num: number) {
    super()
    this.num = num;
  }
  makeNode(uiContext: UIContext): FrameNode | null {
    return null;
  }
}
  1. 使用NodeController的makeNode方法,该方法会在NodeController实例将要绑定NodeContainer时触发,将返回的Frame节点挂载至NodeContainer
typescript 复制代码
makeNode(uiContext: UIContext): FrameNode | null {
    //首先构建BuilderNode实例
    this.itemNode = new BuilderNode(uiContext);
    //调用BuilderNode的build方法,创建组件树
    this.itemNode.build(wrapBuilder<[Params]>(ItemBuilder), new Params(this.num))
    //返回组件树的实体节点
    return this.itemNode.getFrameNode();
  }
  1. 使用NodeContainer显示自定义节点
typescript 复制代码
@Local num: number = 0;
  private itemNodeControl: ItemNodeControl = new ItemNodeControl(this.num);
  build() {
    Column() {
      NodeContainer(this.itemNodeControl)
        .width('100%')
        .height(50)
    }
    .width('100%')
    .height('100%')
  }

1.3.2动态删除

使用条件控制语句可以动态控制NodeContainer节点的移除和显示

typescript 复制代码
if(){
 //....
}else{
 //...
}

动态更新

使用NodeController的makeNode方法和rebuild方法可以切换NodeContainer上绑定的节点

rebuild方法会触发makeNode方法,刷新NodeContainer上显示的节点,若makeNode返回节点为null,则会移除NodeContainer下挂载的节点

typescript 复制代码
replaceBuilderNode(newNode: BuilderNode<Object[]>){
    this.itemNode = newNode;
    this.rebuild()
  }

1.4NodeController的生命周期

  • makeNode:必须重写的方法,用于构建节点树,在即将绑定NodeContainer前调用
  • aboutToResize:当相应的NodeContainer在Measure阶段时调用,入参为节点布局大小
  • aboutToAppear:当相应的NodeContainer在onAppear时调用
  • aboutToDisappear:当相应的NodeContainer在onDisappear时调用
  • onTouchEvent:当相应的NodeContainer在收到Touch事件时调用

2.组件复用

2.1概述

组件复用是指当自定义组件从组件树上移除后被放入缓存池,后续在创建同类型的组件节点时,直接复用缓存池中的组件对象

合理使用可复用组件的好处:

  • 可以避免频繁创建和销毁对象,减少CG次数
  • 另一方面,复用缓存中的组件爱你可以直接更换数据后直接显示,与创建新视图相比,降低了计算开销,提升了显示效率

2.2同一父组件之中实现组件复用

2.2.1实现原理

ArkUI提供了@Reusable来实现自定义组件的复用

  1. 被@Reusable装饰的自定义组件listItem列表项,在画出屏幕一定范围后,被从组件树上移除,组件的对象实例被放入CustomNode虚拟节点中
  2. 在滑动过程中,列表的RecycleManager(回收管理)会将这些CustomNode虚拟节点回收,根据复用表示reuseId分组,形成CachedRecycleNodes,即自定义组件的复用缓存池
  3. 当新的listItem需要在列表上显示时,RecycleManager会优先从复用缓存池里查找对应reuseId的节点,然后将数据绑定到该节点上,并将该节点添加至组件树中

当未使用reuseId属性划分组件时,会默认使用组件名划分

  • @Reusable装饰的组件需要布局在同一个父自定义组件下才能实现缓存复用

2.2.2不同场景下的使用

列表项结构类型相同

实现步骤:

  1. 将列表项封装为自定义组件,并使用@Reusable装饰
  2. 在组件的aboutToReuse方法中实现数据绑定逻辑
typescript 复制代码
  // update data in aboutToReuse method
  aboutToReuse(params: Record<string, Object>): void {
    this.title = params.title as string;
    this.from = params.from as string;
    this.tail = params.tail as string;
  }
  1. 在LazyForEach中使用组件时,设置reuseId

列表项结构类型不同

实现步骤:

  1. 将不同类型的列表项分别封装为自定义组件,添加@Reusable装饰
  2. 在组件内的aboutToResuse回调中进行数据绑定
  3. 在设置reuseId时,根据不同类型的列表项,使用if分别设置reuseId
typescript 复制代码
 LazyForEach(this.dataSource, (item: ItemData) => {
            if (item.type === 0) {
              TextTypeItemView({ item: item })
                .reuseId('text_item_id')
            } else if (item.type === 1) {
              ImageTypeItemView({ item: item })
                .reuseId('image_item_id')
            } else if (item.type === 2) {
              ThreeImageTypeItemView({ item: item })
                .reuseId('three_image_item_id')
            }

组件内子组件可拆分组合

实现步骤:

  1. 将组件可拆分的部分都封装成一个子组件,并添加@Reusable装饰
    如将顶部,内容部,底部分别封装成一个子组件
  2. 进行数据绑定
  3. 实现不同组合对应的@Builder函数
  4. 在LazyForEach中,根据不同的业务逻辑,选择对应类型的@Builder函数

2.3不同父组件之间实现组件复用

2.3.1实现原理

由于复用缓存池需要在同一父组件中,此时需要自定义一个全局的复用缓存池NodePool,利用BuilderNode的节点复用能力,根据页面的状态动态创建、回收、复用子组件,实现跨页面的多个列表间的列表项复用

在ArkUI中,采用Swiper+List+复用缓存池来实现这个功能,由于Swiper中的每个List的列表项的父组件都是List,所以当切换List时,无法直接服用上一个页面的列表项,所以需要使用一个总的复用缓存池

之所以不使用Tab+List,是因为Tabs不支持使用LazyForEach,只能使用ForEach,一次性将TabContent全部创建,子页面切换时也不会触发aboutToDisappear回调,因此不存在组件复用的可能

  1. 在列表项的aboutToDisAppear回调中,根据NodeItem的type类型,存入NodePool中的type类型对应的集合中
  2. 每次需要创建自定义组件时,优先根据type类型查找响应的NodeItem对象,若未查找到则新建一个NodeItem
  3. 组件实体随NodeContainer的生命周期显示时,会执行数据更新,以实现组件的复用

2.3.2使用示例

  1. 实现NodeItem类,继承NodeController并实现makeNode方法

  2. 使用单例模式实现复用缓存池NodePool工具类,该类需要统一管理组件的复用逻辑

    a. 需要实现getNode方法,根据type的不同获取对应的NodeItem方法,未找到则新建

    b. 实现缓存回收方法recycleNode方法,根据type类型存入相应的集合

在getNode方法中,如果找到的NodeItem父节点不为空,需要继续遍历查找下一个有效的NodeItem对象

在reclycle方法中,需要将NodeItem对象的属性重置,使节点内容还原,避免复用显示异常

  1. 将NodeItem包装成为自定义组件,在相应的生命周期中进行缓存、回收、复用
typescript 复制代码
  aboutToAppear(): void {
  //复用
    this.nodeItem = NodePool.getInstance().getNode(this.type, this.item, this.builder!)!;
  }

  aboutToRecycle(): void {
  //回收
    this.nodeItem?.node?.recycle();
  }

  aboutToReuse(params: ESObject): void {
  //缓存
    this.nodeItem?.node?.reuse(params);
  }
  1. 使用@Builder将其包装并对外export
  2. 在列表的LazyForEach中,将@Builder函数wrapBuilder后作为参数传递给NodeItem的自定义组件

2.3.3使用onldle方法预创建组件

有时用户在首次进入页面时耗时可能较高,因为在第一次进入时,组件复用池中没有缓存,所有组件都需要创建,为了优化首次进入的耗时,可以预创建组件,即将组件对象提前放入复用缓存池中

当组件的数量较多时,集中预创建本身也耗时较长,容易造成主线程阻塞。UI提供了onldle接口,该接口会返回每一帧帧尾的空闲时间,可以将组件的预创建分配到每一帧帧尾的空闲时间执行,避免集中运行阻塞主线程

  1. 在NodePool工具类中实现预创建preBuild方法:新建NodeItem实例,设置builder等属性,并调用recycleNode方法提前放入缓存池中
  2. 继承FrameCallback实现帧回调类,在构造方法中传入预创建组件的相关参数,并实现onldle方法
  • 系统会通过onldle回调,将帧尾空闲时间通过参数idleTimeInNano传递出来,可根据单个组件的预创建耗时,设置预创建的剩余空间上限

    i. 当剩余的空闲时间足够创建组件时,则在这一帧中进行组件的预创建,并不断更新当前帧的剩余空闲时间

    ii. 若当前帧的空闲时间不足创建组件,则调用postFrameCallback方法,将创建传递到下一帧,继续进行剩余组件的预创建

  1. 在首页的aboutToAppear回调中执行context.postFrameCallback方法,开启帧回调
typescript 复制代码
  aboutToAppear(): void {
    let dataArray: ItemData[] = [];
    dataArray.push(...genMockItemData(100))
    let context = this.getUIContext();
    context.postFrameCallback(new IdleCallback(context, dataArray));
  }
相关推荐
ifeng09183 小时前
HarmonyOS网络请求优化实战:智能缓存、批量处理与竞态处理
网络·缓存·harmonyos
桦说编程6 小时前
如果让我从头再来学习并发编程
java·设计模式·性能优化
HMSCore9 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 — Notification Kit
harmonyos
HarmonyOS_SDK9 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 — Account Kit
harmonyos
ifeng091811 小时前
HarmonyOS功耗优化实战:减少冗余计算与传感器合理调用
pytorch·华为·harmonyos
爱笑的眼睛1111 小时前
HarmonyOS WebSocket实时通信:从基础原理到复杂场景实践
华为·harmonyos
U***498312 小时前
前端TypeScript教程汇总,从基础到高级
前端·javascript·typescript
n***293214 小时前
前端动画性能优化,减少重绘重排
前端·性能优化
百***355114 小时前
TypeScript 与后端开发Node.js
javascript·typescript·node.js