全局复用:集中化的组件回收与复用

为提升组件回收与复用的性能和内存效率,全局复用池功能允许开发者在任意自定义组件上配置针对指定@Reusable/@ReusableV2复用组件的复用池,该全局复用池优先级高于与父组件绑定的默认复用池。

全局复用池提供了在不同父组件间共享回收组件实例、控制缓存生命周期和大小、以及首次使用前预渲染组件的能力。在阅读本文档之前,建议先阅读@ComponentV2@Component@Reusable@ReusableV2

说明:

全局复用池功能从API版本26.0.0开始支持。

该功能可从API版本26.0.0开始在原子化服务中使用。

概述

在当前实现中,每个@Reusable/@ReusableV2组件的父组件都维护自己的本地复用池。当相同的可复用组件类型被多个同级组件使用时,这会导致复用效率低下,因为一个父组件池中的回收实例无法被另一个父组件复用。

全局复用池通过允许开发者在组件树的上级节点(标注为@Component或@ComponentV2的组件)处配置复用池来解决上述复用效率问题。全局复用池启用后,当复用组件创建或销毁时,框架会向上遍历组件树,为回收和复用操作查找接受指定可复用组件类型的全局复用池,从而支持跨父组件的复用场景,增加复用率,提升复用组件切换的性能。全局复用池的能力:

  • 单个复用池可以为多个子组件提供服务,减少复用池的数量并提高复用率。
  • 开发者可以选择组件类的所有实例共享单个复用池(shared)还是每个实例拥有自己的池(perInstance)。
  • IReusableInfo接口允许应用程序查询和限制缓存组件的数量,包括reuseId等信息。
  • preRender接口允许提前创建可复用组件并将其放入复用池,加快初始渲染速度。
  • 复用组件在被回收或创建时,如果通过遍历父组件未找到匹配的全局复用池,则该组件会使用直接父组件中的默认复用池进行回收和复用。

@Reusable/@ReusableV2默认复用池的局限性

@Reusable和@ReusableV2声明的自定义组件有默认的复用能力,其默认复用池仅存在父组件中,所以当复用的组件是粒度较小的自定义组件,同一复用组件在不同父组件中使用时,无法复用不同父组件下创建的复用组件。

典型应用场景如下,一个父组件下拥有2个可以切换的不同子组件,不同子组件使用了相同的复用组件,这些复用组件的复用池在默认情况下只能存在于子组件中,在子组件切换时,复用池会跟着子组件一起销毁,导致渲染新组件时使用的复用组件只能重新创建,无法从默认复用池中复用已创建的实例。

默认复用池实例代码:

@Entry
@ComponentV2
struct Index {
  @Local componentSwitch: boolean = false;
  build() {
    Column() {
      Button('Switch components')
        .onClick(() => {
          this.componentSwitch = !this.componentSwitch;
        })
      if (this.componentSwitch) { // 切换不同子组件
        ChildComponentA()
      } else {
        ChildComponentB()
      }
    }
  }
}
@ComponentV2
struct ChildComponentA { // ReusableComponent复用组件的复用池默认在组件ChildComponentA上,在Index组件中if分支切换时销毁
  build() {
    Column() {
      Text('Component A')
      ReusableComponent() // 子组件ComponentA和ComponentB共用复用组件ReusableComponent
    }
  }
}
@ComponentV2
struct ChildComponentB {
  build() {
    Column() {
      Text('Component B')
      ReusableComponent() // 子组件ComponentA和ComponentB共用复用组件ReusableComponent
    }
  }
}
@ReusableV2
@ComponentV2
struct ReusableComponent { // 复用组件
  aboutToRecycle() {
    console.info('Reusable component is being recycled');
  }
  aboutToDisappear() {
    console.info('Reusable component is being destroyed'); // 在Index组件中if分支切换时,由于位于父组件ChildComponentA的默认复用池被销毁,该复用组件也会被销毁,无法被ChildComponentB复用。
  }
  build() {
    Text('ReusableComponent')
  }
}

新增全局复用能力后,我们可以在最上层组件Index上声明全局复用池,在if切换组件时,ChildComponentA下的复用组件ReusableComponent能存入Index上的全局复用池,然后在ChildComponentB中的ReusableComponent创建时从全局复用池中取出并复用,避免重复创建复用组件。

适配全局复用能力的示例如下:

@ReusableV2
@ComponentV2
struct ReusableComponent { // 复用组件
  aboutToRecycle() {
    // 在Index组件中if分支切换时,该组件由上层组件Index声明的全局复用池接纳,并复用到ChildComponentB中的ReusableComponent创建过程中
    console.info('Reusable component is being recycled'); 
  }
  aboutToDisappear() {
    console.info('Reusable component is being destroyed'); 
  }
  build() {
    Text('ReusableComponent')
  }
}
@Entry
@ComponentV2({
  reusePool: 'shared', // 配置全局复用池模式,使能全局复用能力
  poolAccepts: [ReusableComponent], // 配置全局复用池接纳名称为ReuseComponent的自定义组件
  freezeWhenInactive: false // 组件冻结默认配置 
})
struct Index {
  @Local componentSwitch: boolean = false;
  build() {
    Column() {
      Button('Switch components')
        .onClick(() => {
          this.componentSwitch = !this.componentSwitch;
        })
      if (this.componentSwitch) { // 切换不同子组件
        ChildComponentA()
      } else {
        ChildComponentB()
      }
    }
  }
}
@ComponentV2
struct ChildComponentA { // ReusableComponent复用组件的复用池使用上层组件Index的全局复用池,在ComponentA上的默认复用池会被跳过
  build() {
    Column() {
      Text('Component A')
      ReusableComponent() // 子组件ComponentA和ComponentB共用复用组件ReusableComponent
    }
  }
}
@ComponentV2
struct ChildComponentB {
  build() {
    Column() {
      Text('Component B')
      ReusableComponent() // 子组件ComponentA和ComponentB共用复用组件ReusableComponent
    }
  }
}

全局复用池与默认复用池对比

类别 默认复用池 全局复用池
声明方式 默认复用池无需声明,当@Reusable或@Reusable装饰的自定义组件被创建或销毁时,会在父组件上创建默认复用池,该复用池能接受任意自定义组件类型。 全局复用池通过在@Component或@ComponentV2中配置reusePoolpoolAccepts开启。
跨分支复用 不支持 支持
池共享 每个父实例有自己的池。 shared模式允许拥有组件类的所有实例共享单个池。
缓存大小控制 不支持 IReusableInfo.maxCount提供按组件、按reuseId的缓存限制。
预渲染 不支持 preRender在首次使用前创建组件。
内存管理 池生命周期与父实例绑定 shared池持续存在直到所有拥有实例被销毁;perInstance池与单个实例绑定。
V1 与 V2 混合 不支持 poolAccepts可同时包含@Reusable复用组件@ReusableV2复用组件。
读取复用池状态 不支持 getReusableInfo可获取当前自定义组件的全局复用池的信息。

装饰器说明

@Component/@ComponentV2配置参数

参数 说明
reusePool ReusePoolOwnership类型的字符串字面量。必须为"shared""perInstance"。决定此组件类的所有实例是共享单个复用池还是每个实例拥有自己的池。必须与poolAccepts一起使用。
poolAccepts 可复用组件名称的数组。列出此复用池接受哪些@Reusable/@ReusableV2组件进行回收。reusePoolpoolAccepts参数必须同时提供。

使用规则

配置复用池

  • reusePoolpoolAccepts参数必须同时提供。仅指定其中一个会导致编译错误。

  • poolAccepts必须是非空数组。每个项必须引用@Reusable或@ReusableV2装饰的自定义组件名称。使用普通(不可复用)组件、@Builder 函数或非组件类会导致编译错误。

  • @Component或@ComponentV2最多只能有一个复用池。

  • reusePoolpoolAccepts配置仅在@Component和@ComponentV2上支持。在@CustomDialog上不受支持。

  • poolAccepts可以同时支持V1和V2可复用组件:数组可以同时包含@Reusable和@ReusableV2装饰的自定义组件名称。

  • 可复用组件不能在自己的全局复用池中直接接受自身组件的名称,否则会导致编译错误。

复用池所有权模式

"shared":拥有@Component/@ComponentV2类的所有实例共享单个复用池实例。

shared复用池的生命周期:

  1. 当拥有组件的第一个实例被创建时,复用池被创建并被该实例引用。
  2. 当拥有组件的第二个实例被创建时,它引用已创建的复用池。不会创建新池。
  3. 当第一个实例被销毁时,复用池不会被销毁,因为它仍被第二个实例引用。
  4. 当第二个(最后一个)实例被销毁时,复用池也被销毁。其中的所有回收组件被删除。
  5. 如果稍后创建拥有组件的新实例,则会创建新的复用池。

注意:shared所有权与static类属性不同。全局复用池有跨实例的引用计数,而非永久单例。

"perInstance":拥有@Component/@ComponentV2的每个实例都有自己的复用池实例。复用池的生命周期与其拥有组件实例的生命周期相同。当拥有组件被销毁时,其复用池和其中的所有回收组件也被销毁。

建议开发者配置shared所有权。这样可以获得更好的复用率和更低的内存占用。

接口说明

有关包括类型定义、参数表、返回值和示例在内的完整API参考,请参阅@ohos.arkui.StateManagement (状态管理)

以下接口可用于全局复用池:

接口 说明
UIUtils.getCustomComponentContext(this).getReusePool() 获取当前组件的IReusePool。如果未配置全局复用池,则返回undefined
IReusePool.getReusableInfo(reusableComp, reuseId?) 检索池中给定可复用组件类型的回收实例信息。支持按reuseId查询。
IReusePool.preRender(builder, n) 调度空闲任务以预创建可复用组件并在首次使用前将其放入复用池。
IReusableInfo.count` 池中当前回收的组件数(只读)。
IReusableInfo.maxCount 允许的最大回收组件数。设置此项可控制缓存大小。
IReusableInfo.reuseId 与此池桶关联的reuseId(只读)。

使用限制

  • @Component和@ComponentV2在配置reusePoolpoolAccepts启用全局复用的同时,需要额外配置freezeWhenInactive参数,freezeWhenInactive参数的取值请参考自定义组件冻结功能(V1)自定义组件冻结功能(V2)

  • getReusableInfopreRender仅在全局复用池实例上可用。它们不能在默认复用池的自定义组件上使用。

  • IReusableInfo.maxCount设置为小于当前count的值会导致异步清除。在延迟期间,count可能暂时超过maxCount

  • 使用preRender预渲染但不被任何池接受的组件会被创建并立即销毁。仅预渲染池配置为接受的组件。

  • 使用"shared"所有权时,只要拥有组件类的任何实例存在,池就会持续存在。如果拥有组件在应用程序的多个部分中使用,回收的组件可能会累积。使用maxCount来控制内存使用。

  • 建议不要在aboutToRecycle中修改会触发重新渲染的状态变量,因为组件此时正从UI树中移除。

  • 由于ArkTS语法限制,poolAccepts参数配置的自定义组件,必须在poolAccepts上方的代码中有定义或者从其他文件导入。如果在poolAccepts传入的组件在下方定义,则会编译报错,报错消息是“Class '...' used before its declaration.”。

使用场景

多个父组件间共享复用池

在此示例中,多个CompA实例为ReusableCompA子组件创建了共享类型的全局复用池。当删除CompA实例时,ReusableCompA子组件被回收到全局复用池中。当添加新的CompA实例时,它从全局复用池中复用组件,避免创建新组件。

@Entry
@ComponentV2
struct Parent {
  @Local show: boolean[] = [true, true, true];

  build() {
    Column({ space: 20 }) {
      Row({ space: 10 }) {
        Button('删除Comp1')
          .onClick(() => this.show[0] = false)
        Button('添加Comp1')
          .onClick(() => this.show[0] = true)
      }
      Row({ space: 10 }) {
        Button('删除Comp2')
          .onClick(() => this.show[1] = false)
        Button('添加Comp2')
          .onClick(() => this.show[1] = true)
      }
      Row({ space: 10 }) {
        Button('删除Comp3')
          .onClick(() => this.show[2] = false)
        Button('添加Comp3')
          .onClick(() => this.show[2] = true)
      }

      Column({ space: 10 }) {
        // 使用if切换触发复用。
        if (this.show[0]) CompA({ label: 'A1' })
        if (this.show[1]) CompA({ label: 'A2' })
        if (this.show[2]) CompA({ label: 'A3' })
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct ReusableCompA {
  @Require @Param value: number;

  aboutToAppear() {
    console.info('ReusableCompA aboutToAppear');
  }
  aboutToReuse() {
    console.info('ReusableCompA aboutToReuse');
  }
  aboutToRecycle() {
    console.info('ReusableCompA aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('ReusableCompA aboutToDisappear');
  }

  build() {
    Text(`ReusableCompA Item ${this.value}`)
      .fontSize(16)
      .fontColor(Color.Green)
  }
}

// 多个CompA组件实例共用一个ReusableCompA的全局复用池。
@ComponentV2({ reusePool: 'shared', poolAccepts: [ReusableCompA], freezeWhenInactive: false})
struct CompA {
  @Require @Param label: string;

  build() {
    Column({ space: 5 }) {
      Text(`CompA ${this.label}`)
        .fontSize(18)
        .fontColor(Color.Blue)
      ReusableCompA({ value: 1 })
      ReusableCompA({ value: 2 })
    }
      .border({ width: 1, color: Color.Gray })
      .padding(5)
  }
}

arkts-global-reuse-shared.gif

启动 — 3个CompA实例,6个ReusableCompA子组件:

CompA aboutToAppear (×3)
ReusableCompA aboutToAppear (×6)

删除 Comp1 — 子组件被回收:

CompA aboutToDisappear
ReusableCompA aboutToRecycle (×2)

添加 Comp1 — 子组件从共享池中复用:

CompA aboutToAppear
ReusableCompA aboutToReuse (×2)

依次删除所有 3 个 CompA — 当最后一个CompA被销毁时,没有剩余的CompA实例,因此共享池也被销毁:

// 删除 Comp1 和 Comp2:子组件被回收
ReusableCompA aboutToRecycle (×2, 每个被删除的 CompA)

// 删除 Comp3:最后一个实例 — 共享池被销毁
ReusableCompA aboutToDisappear (×6, 所有缓存实例被永久销毁)

使用@Provider/@Consumer的独立复用池

此示例演示与特定父实例绑定的perInstance池。它还展示了@Consumer在复用周期后如何重连到@Provider

@ReusableV2
@ComponentV2
struct ReusableChild {
  @Consumer() provide: number = 10;

  aboutToAppear() {
    console.info('ReusableChild aboutToAppear');
  }
  aboutToReuse() {
    console.info('ReusableChild aboutToReuse');
    // 组件被复用时修改@Consumer状态变量,能同步该数据到Parent组件的@Provider状态变量中。
    this.provide = 150;
  }
  aboutToRecycle() {
    console.info('ReusableChild aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('ReusableChild aboutToDisappear');
  }

  build() {
    Column({ space: 20 }) {
      Text(`ReusableChild @Consumer: ${this.provide}`)
        .fontSize(20)
      SubChild()
    }
  }
}

@Entry
// 声明全局复用池,接纳ReusableChild复用组件。
@ComponentV2({ reusePool: 'perInstance', poolAccepts: [ReusableChild], freezeWhenInactive: false })
struct Parent {
  @Provider() provide: number = 100;
  @Local boolVal: boolean = false;

  build() {
    Column({ space: 10 }) {
      Button('切换组件')
        .onClick(() => {
          this.boolVal = !this.boolVal;
        })

      Text(`父组件 Provider: ${this.provide}`)
        .fontSize(20)
      Button('增加 +10')
        .onClick(() => {
          this.provide += 10;
        })

      // 切换到可复用组件时,ReusableChild会进入当前组件的全局复用池中。
      if (this.boolVal) {
        Text('非可复用组件')
          .fontSize(24)
        Child()
      } else {
        Text('可复用组件')
          .fontSize(24)
        ReusableChild()
      }
    }
    .width('100%')
  }
}

@ComponentV2
struct SubChild {
  @Consumer() provide: number = 10;

  aboutToAppear() {
    console.info('SubChild aboutToAppear');
  }
  aboutToReuse() {
    console.info('SubChild aboutToReuse');
  }
  aboutToRecycle() {
    console.info('SubChild aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('SubChild aboutToDisappear');
  }

  build() {
    Text(`SubChild @Consumer: ${this.provide}`)
  }
}

@ComponentV2
struct Child {
  @Consumer() provide: number = 10;

  aboutToAppear() {
    console.info('Child aboutToAppear');
  }
  aboutToDisappear() {
    console.info('Child aboutToDisappear');
  }

  build() {
    Column({ space: 20 }) {
      Text(`Child @Consumer: ${this.provide}`)
        .fontSize(20)
      SubChild()
    }
  }
}

arkts-global-reuse-per-instance.gif

从ReusableChild切换到Child

ReusableChild aboutToRecycle   // 进入池
SubChild aboutToRecycle        // 子树级联
Child aboutToAppear            // 非可复用,全新创建
SubChild aboutToAppear         // Child内的新SubChild

从Child切换到ReusableChild

Child aboutToDisappear         // 非可复用,永久销毁
SubChild aboutToDisappear      // 随Child一起销毁
ReusableChild aboutToReuse     // 从池中检索,@Consumer重连
SubChild aboutToReuse          // 子树级联

复用后,aboutToReuse回调设置this.provide = 150@Consumer重连到Parent组件的@Provider。后续@Provider更新正确传播到复用的组件。

使用getReusableInfo检查和控制池

此示例演示如何使用getReusableInfo接口在运行时检查池状态和控制缓存大小。

import { UIUtils, IReusableInfo } from '@kit.ArkUI';

@ReusableV2
@ComponentV2
struct GlobalChild {
  @Consumer() provide: number = 10;

  aboutToAppear() {
    console.info('GlobalChild aboutToAppear');
  }
  aboutToReuse() {
    console.info('GlobalChild aboutToReuse');
  }
  aboutToRecycle() {
    console.info('GlobalChild aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('GlobalChild aboutToDisappear');
  }

  build() {
    Column({ space: 20 }) {
      Text('全局子组件')
      SubChild()
    }
  }
}

@ReusableV2
@ComponentV2
struct LegacyComp {
  aboutToAppear() {
    console.info('LegacyComp aboutToAppear');
  }
  aboutToReuse() {
    console.info('LegacyComp aboutToReuse');
  }
  aboutToRecycle() {
    console.info('LegacyComp aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('LegacyComp aboutToDisappear');
  }

  build() {
    Column() {
      Text('传统组件')
      ReusableChild()
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableChild {
  @Consumer() provide: number = 10;
  aboutToAppear() {
    console.info('ReusableChild aboutToAppear');
  }
  aboutToReuse() {
    console.info('ReusableChild aboutToReuse');
  }
  aboutToRecycle() {
    console.info('ReusableChild aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('ReusableChild aboutToDisappear');
  }

  build() {
    Text(`ReusableChild @Consumer: ${this.provide}`)
  }
}

@ReusableV2
@ComponentV2
struct SubChild {
  @Consumer() provide: number = 10;
  aboutToAppear() {
    console.info('SubChild aboutToAppear');
  }
  aboutToReuse() {
    console.info('SubChild aboutToReuse');
  }
  aboutToRecycle() {
    console.info('SubChild aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('SubChild aboutToDisappear');
  }

  build() {
    Text(`SubChild @Consumer: ${this.provide}`)
  }
}

@Entry
@ComponentV2({ reusePool: 'perInstance', poolAccepts: [LegacyComp, GlobalChild, ReusableChild, SubChild], freezeWhenInactive: false })
struct Index {
  @Provider() provide: number = 100;
  @Local boolVal: boolean = true;

  verifyPool(compName: string, comp: Function) {
    const pool = UIUtils.getCustomComponentContext(this).getReusePool();
    if (!pool) {
      console.info('Cannot find pool.');
      return;
    }
    const ret = pool.getReusableInfo(comp);
    if (ret === undefined) {
      console.info(`getReusableInfo(${compName}): undefined`);
    } else if (Array.isArray(ret)) {
      console.info(`getReusableInfo(${compName}): Array[${ret.length}]`);
      ret.forEach((info: IReusableInfo, i: number) => {
        console.info(`  [${i}] reuseId=${info.reuseId}, count=${info.count}, maxCount=${info.maxCount}`);
      });
    } else {
      console.info(`getReusableInfo(${compName}): reuseId=${ret.reuseId}, count=${ret.count}, maxCount=${ret.maxCount}`);
    }
  }

  setPoolMaxCount(compName: string, comp: Function) {
    const pool = UIUtils.getCustomComponentContext(this).getReusePool();
    if (!pool) {
      console.info('Cannot find pool.');
      return;
    }
    const ret = pool.getReusableInfo(comp);
    if (ret && !Array.isArray(ret)) {
      // maxCount赋值为0时会释放复用池中的组件。
      ret.maxCount = 0;
    }
  }

  build() {
    Column({ space: 10 }) {
      Button('切换组件')
        .onClick(() => {
          this.boolVal = !this.boolVal;
        })
        .width(150)

      // 手动池检查按钮
      Button('检查GlobalChild')
        .onClick(() => this.verifyPool('GlobalChild', GlobalChild))
        .width(150)
      Button('检查LegacyComp')
        .onClick(() => this.verifyPool('LegacyComp', LegacyComp))
        .width(150)
      Button('设置复用池大小')
        .onClick(() => this.setPoolMaxCount('LegacyComp', LegacyComp))
        .width(150)

      if (this.boolVal) {
        GlobalChild()
      } else {
        LegacyComp()
      }
    }
    .width('100%')
  }
}

步骤1 — 启动(GlobalChild可见):

点击"检查GlobalChild":count=0, maxCount=100(GlobalChild可见,不在池中)。

点击"检查LegacyComp":count=0, maxCount=100(LegacyComp不可见,不在池中)。

步骤2 — 切换到LegacyComp

GlobalChild aboutToRecycle    // 进入池
SubChild aboutToRecycle       // 和GlobalChild一起进入复用池
LegacyComp aboutToAppear      // 全新创建
ReusableChild aboutToAppear

点击"检查GlobalChild":count=1, maxCount=100(回收到池中)。

点击"检查LegacyComp":count=0, maxCount=100(可见,不在池中)。

步骤3 — 切换回GlobalChild

LegacyComp aboutToRecycle
ReusableChild aboutToRecycle
GlobalChild aboutToReuse      // 从池中复用
SubChild aboutToReuse         // 和GlobalChild一起被复用

点击"检查LegacyComp":count=1, maxCount=100(现在回收到池中)。

步骤4 — 点击设置复用池大小

LegacyComp aboutToDisappear
ReusableChild aboutToRecycle

再点击"检查LegacyComp": count=0, maxCount=0(复用池被手动清空了)

arkts-global-reuse-getreusableinfo.gif

使用reuseId控制缓存大小

当使用不同的reuseId值回收组件时,getReusableInfo返回可以独立控制的每个桶的信息。

import { UIUtils, IReusableInfo } from '@kit.ArkUI';

@ReusableV2
@ComponentV2
struct TestChild {
  @Param label: string = '';
  aboutToAppear() {
    console.info(`TestChild [${this.label}] aboutToAppear`);
  }
  aboutToReuse() {
    console.info(`TestChild [${this.label}] aboutToReuse`);
  }
  aboutToRecycle() {
    console.info(`TestChild [${this.label}] aboutToRecycle`);
  }
  aboutToDisappear() {
    console.info(`TestChild [${this.label}] aboutToDisappear`);
  }
  build() {
    Text(`子组件: ${this.label}`)
  }
}

@Entry
@ComponentV2({ reusePool: 'perInstance', poolAccepts: [TestChild], freezeWhenInactive: false })
struct PoolOwner {
  @Local showA: boolean = true;
  @Local showB: boolean = true;
  @Local showC: boolean = true;

  purgeReuseId(id: string) {
    const pool = UIUtils.getCustomComponentContext(this).getReusePool();
    const info = pool?.getReusableInfo(TestChild, id) as IReusableInfo;
    if (info) {
      info.maxCount = 0;  // 仅释放全局复用池中此reuseId对应的复用组件
    }
  }

  // 打印当前组件复用池信息
  printReusePool() {
    const pool = UIUtils.getCustomComponentContext(this).getReusePool();
    const info = pool?.getReusableInfo(TestChild) as IReusableInfo[];
    if (info) {
      info.forEach((item) => {
        console.info(`{ count: ${item.count}, maxCount: ${item.maxCount}, reuseId: ${item.reuseId} }`);
      })
    }
  }

  build() {
    Column({ space: 3 }) {
      Button('切换A')
        .onClick(() => {
          this.showA = !this.showA;
        })
        .width(150)
      Button('切换B')
        .onClick(() => {
          this.showB = !this.showB;
        })
        .width(150)
      Button('切换C')
        .onClick(() => {
          this.showC = !this.showC;
        })
        .width(150)
      Button('仅清除B')
        .onClick(() => this.purgeReuseId('B'))
        .width(150)
      Button('打印复用池信息')
        .onClick(() => this.printReusePool())
        .width(150)

      if (this.showA) {
        TestChild({ label: 'A' })
          .reuse({ reuseId: () => 'A' })
      }
      if (this.showB) {
        TestChild({ label: 'B' })
          .reuse({ reuseId: () => 'B' })
      }
      if (this.showC) {
        TestChild({ label: 'C' })
          .reuse({ reuseId: () => 'C' })
      }
    }
    .width('100%')
  }
}

arkts-global-reuse-reuseid.gif

当所有3个都被关闭时,getReusableInfo(TestChild)(不带reuseId)返回一个数组:

  { count: 0, maxCount: 100, reuseId: undefined }  // 始终包含
  { count: 1, maxCount: 100, reuseId: 'A' }
  { count: 1, maxCount: 100, reuseId: 'B' }
  { count: 1, maxCount: 100, reuseId: 'C' }

点击"仅清除B"(设置B的maxCount = 0)后,只有B的实例被释放。数组变为:

  { count: 0, maxCount: 100, reuseId: undefined }
  { count: 1, maxCount: 100, reuseId: 'A' }
  { count: 0, maxCount: 0, reuseId: 'B' } // maxCount非默认时也会显示
  { count: 1, maxCount: 100, reuseId: 'C' }

全部重新打开:

  • A和C触发aboutToReuse(从池中复用)。
  • B触发aboutToAppear(新实例)。

查询不存在的reuseId(例如,pool.getReusableInfo(TestChild, 'X'))返回单个对象,其中 count: 0, maxCount: 100

跨分支复用

此示例演示全局复用池的主要优势:从组件树的一个分支回收的组件可以在完全不同的分支中复用。

@Entry
@ComponentV2
struct Parent {
  @Local showIndex1: boolean = true;
  @Local showIndex2: boolean = true;

  build() {
    Column({ space: 20 }) {
      Row({ space: 10 }) {
        Button('切换Index1')
          .onClick(() => { this.showIndex1 = !this.showIndex1 })
        Button('切换Index2')
          .onClick(() => { this.showIndex2 = !this.showIndex2 })
      }
      if (this.showIndex1) { 
        Index1() 
      }
      if (this.showIndex2) {
        Index2()
      }
    }
    .width('100%')
  }
}

@ReusableV2
@ComponentV2
struct Index1 {
  build() {
    Column() {
      ChildComp()
    }
  }
}

@ReusableV2
@ComponentV2
struct ChildComp {
  build() {
    Column() {
      Text('ChildComp')
      ReusableChild({ source: 'Index1 -> ChildComp' })
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableChild {
  @Require @Param source: string;
  @Local instanceId: number = Math.floor(Math.random() * 1000);

  aboutToAppear() {
    console.info(`ReusableChild[${this.instanceId}] appear source=${this.source}`);
  }
  aboutToRecycle() {
    console.info(`ReusableChild[${this.instanceId}] recycle source=${this.source}`);
  }
  aboutToReuse() {
    console.info(`ReusableChild[${this.instanceId}] reuse source=${this.source}`);
  }
  aboutToDisappear() {
    console.info(`ReusableChild[${this.instanceId}] disappear source=${this.source}`);
  }

  build() {
    Text(`ReusableChild 实例 ${this.instanceId} (${this.source})`)
  }
}

@ComponentV2({ reusePool: 'shared', poolAccepts: [ReusableChild], freezeWhenInactive: false })
struct Index2 {
  @Local showChild: boolean = true;

  build() {
    Column() {
      Text('Index2组件')
      Button('切换ReusableChild')
        .onClick(() => { 
          this.showChild = !this.showChild;
        })
      if (this.showChild) {
        ReusableChild({ source: 'Index2' })
      }
    }
  }
}

arkts-global-reuse-cross-branch.gif

测试序列

  1. 关闭Index1 — 来自Index1分支的ReusableChild 进入共享池。
  2. 关闭再打开Index2的ReusableChild — 池化的实例(最初来自Index1)在Index2中被复用。
  3. 打开Index1 — Index1和ChildComp被复用。ReusableChild从池中被复用。

多级复用池结构

当在组件树的不同级别存在多个复用池配置时,每个可复用组件路由到接受它的最近的祖先池。

@ReusableV2
@ComponentV2
struct ChildA {
  aboutToAppear() {
    console.info('ChildA aboutToAppear');
  }
  aboutToReuse() {
    console.info('ChildA aboutToReuse');
  }
  aboutToRecycle() {
    console.info('ChildA aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('ChildA aboutToDisappear');
  }

  build() {
    Column({ space: 8 }) {
      Text('子组件 A')
        .fontColor(Color.Blue)
      // 复用组件ChildA中包含复用组件ReusableLeaf
      ReusableLeaf()
    }
  }
}

@ReusableV2
@ComponentV2
struct ReusableLeaf {
  aboutToAppear() {
    console.info('ReusableLeaf aboutToAppear');
  }
  aboutToReuse() {
    console.info('ReusableLeaf aboutToReuse');
  }
  aboutToRecycle() {
    console.info('ReusableLeaf aboutToRecycle');
  }
  aboutToDisappear() {
    console.info('ReusableLeaf aboutToDisappear');
  }

  build() {
    Text('可复用叶子节点')
      .fontColor(Color.Green)
  }
}

@Entry
// 配置全局复用池,接纳ChildA复用组件
@ComponentV2({ reusePool: 'shared', poolAccepts: [ChildA], freezeWhenInactive: false })
struct EntryComp {
  @Local showParent: boolean = true;

  build() {
    Column({ space: 15 }) {
      Text('入口组件')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
      Button('切换 ParentA')
        .onClick(() => {
          // 修改if条件触发组件复用
          this.showParent = !this.showParent;
        })
      // 切换if分支后,ParentA中的ChildA进入EntryComp的全局复用池
      if (this.showParent) {
        ParentA()
      }
    }
    .width('100%')
  }
}

// 配置全局复用池,接纳ReusableLeaf复用组件
@ComponentV2({ reusePool: 'perInstance', poolAccepts: [ReusableLeaf], freezeWhenInactive: false })
struct ParentA {
  @Local showChild: boolean = true;

  aboutToAppear() {
    console.info('ParentA aboutToAppear');
  }

  build() {
    Column({ space: 8 }) {
      Text('父组件 A')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      Button('切换 ChildA')
        .onClick(() => { 
          // 修改if条件触发子组件ChildA复用
          this.showChild = !this.showChild;
        })
      // 切换if分支后,ChildA进入EntryComp的全局复用池,ReusableLeaf节点跟随ChildA存入EntryComp复用池中。
      if (this.showChild) {
        ChildA()
      }
    }
  }
}

arkts-global-reuse-multi-level.gif

  • ChildA使用EntryComp上声明的全局复用池,因为EntryComp复用池配置poolAccepts接受ChildA
  • ReusableLeaf和它的父组件ChildA一起进入EntryComp的复用池中,不会进入ParentA上配置的全局复用池。

关闭/打开ChildAChildAReusableLeaf都从各自的池中回收和复用:

ChildA aboutToRecycle / aboutToReuse         // EntryComp的池
ReusableLeaf aboutToRecycle / aboutToReuse   // EntryComp的池

关闭ParentA(当ChildA在池中时)— ParentA被销毁,但ParentA复用池为空,不会触发ReusableLeaf的组件销毁。

打开ParentA — ParentA被全新创建。ChildA从EntryComp的池中复用,ReusableLeaf也被复用:

ParentA aboutToAppear           // 新实例
ChildA aboutToReuse             // 从EntryComp的复用池中取出
ReusableLeaf aboutToReuse       // 从EntryComp的复用池中取出

使用preRender预渲染组件

preRender用于提前创建可复用组件实例并将其放入复用池,后续创建时可直接复用。

import { UIUtils } from '@kit.ArkUI';

@ReusableV2
@ComponentV2
struct ReusableComponent {
  @Require @Param param: number;

  aboutToAppear() {
    console.info('ReusableComponent aboutToAppear');
  }
  aboutToReuse() {
    console.info('ReusableComponent aboutToReuse');
  }

  build() {
    Column() {
      Text(`ReusableComponent ${this.param}`)
    }
  }
}

@Builder 
function preRenderBuilder() {
  ReusableComponent({ param: 0 })
}

@Entry
@ComponentV2({ reusePool: 'shared', poolAccepts: [ReusableComponent], freezeWhenInactive: false })
struct Index {
  @Local onUIFullyLoaded: boolean = false;

  aboutToAppear() {
    // 获取池并调度预渲染。
    const pool = UIUtils.getCustomComponentContext(this).getReusePool();
    pool!.preRender(new WrappedBuilder<[]>(preRenderBuilder.bind(this)), 1)
      .then(() => {
        this.onUIFullyLoaded = true;
      });
  }

  build() {
    Column() {
      CompA({ showFullUI: this.onUIFullyLoaded })
    }
  }
}

@ComponentV2
struct CompA {
  @Require @Param showFullUI: boolean;
  @Local param: number = 8;

  build() {
    if (this.showFullUI) {
      ReusableComponent({ param: this.param })
    }
  }
}

arkts-global-reuse-prerender.png

执行序列:

  1. 启动时,Index.aboutToAppear()通过UIUtils.getCustomComponentContext(this).getReusePool()获取池并调用preRender
  2. preRender作为空闲任务异步执行:它调用@Builder函数,创建ReusableComponent实例。
  3. 预渲染的ReusableComponent被回收到Index的复用池中。
  4. 设置onUIFullyLoaded = true,这会触发CompA的重新渲染。
  5. CompA的if条件变为true。框架创建ReusableComponent时,能找到Index上的全局复用池,并取出预渲染的实例。
  6. aboutToReuse触发,组件被复用,无需全新创建。

说明:

preRender仅将池配置为接受的组件放入其中。预渲染复用池不接受的组件会立即创建并销毁。预渲染期间不会从全局池中复用组件;复用池仅接受新创建的实例。