【HarmonyOS】组件复用
目录
概述
组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。
在应用开发时,组件复用时优化UI性能,确保应用流畅的重要手段。合理使用可复用组件,一方面,可以避免频繁创建和销毁对象的过程,减少内存回收的频率;另一方面,复用缓存中的组件可以直接绑定数据进行显示,与创建新视图相比,降低了计算开销,提升了显示效率。
常见的组件复用开发场景时长列表滑动:在应用展示大量数据的列表界面中,当用户快速地进行滑动操作,列表项反复创建销毁可能导致卡顿等性能问题。这种情况下,使用组件复用机制可以重用已经创建过地列表项视图,提高滑动地流畅度。
场景一:同一列表内的组件复用
场景描述
同一列表项内的列表项组件复用时典型的开发场景。列表在滑动时,超出屏幕一定范围的列表项,被放入缓存池中,当新的列表项滑动进入屏幕范围内,从缓存池中取出对象,绑定对应数据后呈现到列表界面中。
在实际业务中,同一列表内可能呈现一种或多种不同结构的列表项。

实现原理
ArkUI提供了@Reusable装饰器以实现自定义组件的复用,其原理如图所示:

-
标记了@Reusable的自定义组件listItem列表项,在滑动出屏幕一定范围后,从组件树上被移除,组件的对象实例被放入CustomNode虚拟节点(与自定义组件一一对应的自定义节点)。
-
在不断滑动过程中,列表的RecycleManager将这些CustomNode虚拟结点回收,根据复用标识reuseId分组,形成CachedRecycleNodes的集合,即视图对象的复用缓存池。
reuseId用于标记自定义组件复用组,当组件回收复用时,复用框架将根据组件的reuseId来划分组件的复用组。
-
继续滑动,新的listItem需要在列表上显示时,RecycleManager优先从复用缓存池(CachedRecycleNodes集合)中查找对应reuseId的视图对象,然后将新的数据绑定到该视图,重用该结点并添加到组件树上。
开发步骤
-
定义可复用组件:使用@Reusable装饰器修饰可复用的自定义组件。
-
实现复用回调:可复用组件需要实现aboutToReuser()生命周期回调。当组件从缓存中重新加入节点树时,触发aboutToReuse()生命周期回调,组件的构造参数会传递进来,开发者根据需要在回调中处理数据刷新。
-
布局中使用可复用组件:设置reuseId划分组件的复用组件,以区分缓存池。未设置reuseId时,组件名会默认作为reuseId。
@Entry @Component struct Index { // ... build() { Column() { // ... if (this.switch) { // 3.layout the component and set reuse id ReusableComponent({ text: this.typeStr }) .reuseId(this.typeStr) } } // ... } // ... } // 1.add @Reusable to mark component @Reusable @Component struct ReusableComponent { @State text: string = '' // 2.update data in aboutToReuse aboutToReuse(params: Record<string, Object>): void { this.text = params.text as string; } build() { // ... } }@Reusable修饰的组件需要布局在同一个父自定义组件下才能实现缓存复用
不见在@Resuable修饰的组件中嵌套使用另一个@Reusable组件
列表结构类型相同
实现步骤:
-
将列表项封装为自定义组件ItemView,添加@Reusable修饰。
-
在ItemView组件内的aboutToReuse()方法中进行新数据绑定逻辑。
-
在列表的LazyForEach()中使用ItemView组件,设置reuseId
@Component export struct OneTypeItemPage { // ... build() { NavDestination() { Column() { List() { LazyForEach(this.dataSource, (item: ItemData) => { // layout the component, and set reuse id (or no set with using name as default id) ItemView({ title: item.title, from: item.from, tail: item.tail }) .reuseId('item_id') }, (item: ItemData) => item.id.toString()) } // ... } // ... } // ... } } // add @Reusable to mark component @Reusable @Component struct ItemView { @State title: string | Resource = ''; @State from: string | Resource = ''; @State tail: string | Resource = ''; // 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; } build() { // ... } }
列表项结构类型不同
这种场景下,列表中会有多种类型的列表项,如下图包含了文本、单图、多图等三种列表项、其布局、组成元素存在一定的差异,可以将每种类型的列表项分别作为复用单位。
在滑动过程中,不同类型的列表项将分别回收进入各自的缓存池,当需要复用时,根据类型找到对应视图缓存进行显示。
实现步骤:
- 将不同类型的列表项分别封装为自定义组件,添加@Reusable修饰。
- 在组件内的aboutToReuse()方法进行新的数据绑定逻辑。
- 在列表的LazyForEach()中,根据业务逻辑进行if条件选择,布局相应类型的列表项组件,分别设置reuseId。
@Component
export struct MultiTypeItemPage {
// ...
build() {
NavDestination() {
Column() {
List() {
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')
}
}, (item: ItemData) => item.id.toString())
}
// ...
}
// ...
}
// ...
}
}
@Reusable
@Component
struct TextTypeItemView {
// ...
}
@Reusable
@Component
struct ImageTypeItemView {
// ...
}
@Reusable
@Component
struct ThreeImageTypeItemView {
// ...
}
列表内子组件可拆分组合
这种情况下,列表项也具有多种结构类型。通过观察可知,列表项内部子组件都是纵向分布排列,相同之处是啥顶部的文本标题、底部的发布时间,而不同之处是中间的区域部分:有单图、多图、视频三种情况。

因此,可以创建物种复用子组件,通过子足迹按的选择组合,实现不同类型的列表项。

实现步骤:
-
将单图、多图、视频、顶部标题、底部时间等分别封装为子组件,添加@Reusable修饰。
-
在组件内的aboutToReuse()方法中进行新的数据绑定逻辑。
-
通过组合子组件,实现三个不同的@Builder函数,与三种列表项一一对应。
-
在列表的LazyForEach中,根据业务逻辑进行条件选择,分别调用相应类型的@Builder函数。
为什么使用@Builder实现,而不直接使用自定义组件嵌套子组件?
由于缓存池位于自定义组件上,嵌套子组件后会将缓存池分割,导致复用不生效。
而使用@Builder可以是内部的自定义组件依然汇聚在同一个缓存池里,从而实现相互复用
@Component
export struct ComposableItemPage {
// ...
@Builder
itemBuilderSingleImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
@Builder
itemBuilderThreeImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
@Builder
itemBuilderVideoImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleVideoView({ item: item }).reuseId('middle_video_id')
BottomView({ item: item }).reuseId('bottom_id')
}
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
Column() {
if (item.type === 0) {
this.itemBuilderSingleImage(item)
} else if (item.type === 1) {
this.itemBuilderThreeImage(item)
} else if (item.type === 2) {
this.itemBuilderVideoImage(item)
}
}
// ...
}
}, (item: ItemData) => item.id.toString())
}
// ...
}
// ...
}
// ...
}
}
@Reusable
@Component
struct TopView {
// ...
}
@Reusable
@Component
struct BottomView {
// ...
}
@Reusable
@Component
struct MiddleSingleImageView {
// ...
}
@Reusable
@Component
struct MiddleThreeImageView {
// ...
}
@Reusable
@Component
struct MiddleVideoView {
// ...
}
场景: 多个列表间的组件复用
场景描述
应用开发有这种场景:在不同的标题页面中展示数据,每一页面下实现了一个列表,这样在页面切换时,列表与列表之间如果存在结构相同的列表项,就有组件复用的优化可能。例如下图,News、Hot等页签下,绘制了类型相同的列表项。

实现原理
在ArkUI中,可以采用Swiper+List实现这种功能场景,其中Swiper中的每个页面都是用一个List列表呈现内容。从@Reusable的复用机制可知,复用缓存池需要在同一父组件中,而列表项Item的父组件是当前页面的列表List,当Swiper内的页面切换时,无法直接复用上一个页面的列表项。
此时可以自定义一个全局的复用缓存池NodePool,利用BuilderNode的节点复用能力,根据页面状态创建、回收、复用子组件,实现这种跨页面多个列表间的组件复用。
为什么不使用Tabs+List,而是Swiper+List组件实现?
当前Tabs内容不支持LazyForEach(),只能使用ForEach+TabContent。如果使用ForEach(),Tabs页面显示时会一次性将所有的TabContent创建,TabContent子页面切换时也不会执行aboutToDisappear(),无法回收组件,进而不存在复用优化的可能。
在需要布局自定义组件的位置,使用NodeContainer占位,然后继承NodeController实现NodeItem结点类,其内部需要持有BuilderNode实例以实现结点的创建和复用,同时需要持有视图相应的数据对象以更新界面显示。
- 当NodeItem随着视图组件即将销毁时,在aboutToDisappear()中回收NodeItem到NodePool缓存池,存入type类型对应的集合中。
- 每次需要创建自定义组件时,优先根据type类型查找对应的NodeItem对象,若未找到则新建一个NodeItem。
- 视图组件随着NodeContainer的生命周期显示时,执行数据更新,完成组件的复用过程。
NodeContainer 基础组件,用于挂载自定义节点(BuilderNode或FrameNode),并通过NodeController动态控制节点的上树和下树。组件不支持尾随添加子节点,接受一个NodeController实例接口,需要NodeController组合使用。
NodeController用于实现自定义节点的创建、显示、更新等操作的管理,并负责将自定义节点挂载到NodeContainer上。
开发步骤
-
实现列表项占位结点类NodeItem,继承NodeController实现makeNode()方法,根据node是否存在,执行创建或刷新数据的逻辑,并在aboutToDisappear()时回收组件结点。
import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; import { HashMap, LinkedList } from '@kit.ArkTS'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; const DOMAIN = 0x0000; // [Start prebuild1] // [Start node1] export class NodeItem extends NodeController { // [StartExclude prebuild1] // 用于构建节点的Builder函数 public builder: WrappedBuilder<ESObject> | null = null; // BuilderNode实例,管理实际UI节点 public node: BuilderNode<ESObject> | null = null; // 节点绑定的数据 public data: ESObject = {}; // 节点类型标识,用于分类缓存 public type: string = ''; // id 节点唯一标识 public id: number = 0; aboutToDisappear(): void { // recycle node into cache pool when UI disappear // 当节点从UI消失时,自动回收到缓存池 NodePool.getInstance().recycleNode(this.type, this); } update(data: ESObject) { this.data = data; this.node?.reuse(data); // 重用节点并更新数据 } makeNode(uiContext: UIContext): FrameNode | null { // build new node or update node in the cache if (!this.node) { // 新建节点 this.node = new BuilderNode(uiContext); this.node.build(this.builder, this.data); } else { // 复用已有节点 this.update(this.data); } return this.node.getFrameNode(); } // [EndExclude prebuild1] // [StartExclude node1] // 在后台线程预构建节点,避免UI渲染时的构建开销 prebuild(uiContext: UIContext) { this.node = new BuilderNode(uiContext); this.node.build(this.builder, this.data); } // [EndExclude node1] } // [End node1] -
使用单例模式实现复用缓存池NodePool工具类,在应用内统一管理组件的复用逻辑:实现取缓存getNode()方法,根据传入的type类型,获取对应的NodeItem,如果未找到,则新创建后绑定数据;实现缓存回收recycleNode()方法,根据type类型存入相应的集合中。
- getNode()方法中,如果找到的NodeItem父结点不为空(说明未完全下树),选哟继续遍历查找下一个有效的NodeItem对象。
- recycleNode()方法中,需要对NodeItem对象属性重置,使节点内容还原,避免复用显示异常情况。
// [Start node2] export class NodePool { // [StartExclude prebuild1] private static instance: NodePool; private idGen: number; // hashmap 键是节点类型,值是该类型的节点链表 // LinkedList 存储同类型的可复用节点 private nodePool: HashMap<string, LinkedList<NodeItem>>; private constructor() { this.nodePool = new HashMap(); this.idGen = 0; } // single instance mode, managing the cache pool public static getInstance() { if (!NodePool.instance) { NodePool.instance = new NodePool(); } return NodePool.instance; } public getNextId(): number { this.idGen += 1; return this.idGen; } public getNode(type: string, item: ESObject, builder: WrappedBuilder<ESObject>): NodeItem | undefined { let nodeItem: NodeItem | undefined = undefined; try { // get the cached node based on type // 从缓存池查找可复用节点 if (this.nodePool.get(type)) { for (let i = 0; i < this.nodePool.get(type)?.length; i++) { let tmpItem: NodeItem | undefined = this.nodePool.get(type)?.get(i); // if the parent node is null, it means the node is reusable, so get it out // 节点没有父结点才可复用 if (!tmpItem.node?.getFrameNode()?.getParent()) { nodeItem = tmpItem; this.nodePool.get(type)?.removeByIndex(i); break; } } } } catch (e) { let err = e as BusinessError; hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`); } // 没有找到可复用节点,创建新的 if (!nodeItem) { // No valid reusable node found, so create new one nodeItem = new NodeItem(); nodeItem.builder = builder; nodeItem.type = type; nodeItem.data.item = item; } else { // 找到可复用节点,更新数据 // update cached node nodeItem.data.item = item; } return nodeItem; } // cache the node based on type // 回收节点到缓存池 public recycleNode(type: string, node: NodeItem) { try { let nodeArray: LinkedList<NodeItem> = this.nodePool.get(type); if (!nodeArray) { nodeArray = new LinkedList(); this.nodePool.set(type, nodeArray); } // reset data // 重置数据 node.data.item = {}; nodeArray.add(node); } catch (e) { let err = e as BusinessError; hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`); } } // [EndExclude prebuild1] // [StartExclude node2] // 预构建并缓存节点 public preBuild(type: string, item: ESObject, builder: WrappedBuilder<ESObject>, uiContext: UIContext) { if (type) { let nodeItem: NodeItem | undefined = new NodeItem(); nodeItem.builder = builder; nodeItem.data.item = item; nodeItem.type = type; nodeItem.prebuild(uiContext); // 预构建 this.recycleNode(type, nodeItem); // 放入缓存池 } } // [EndExclude node2] } // [End node2] // [End prebuild1] -
将步骤1中的列表项占位节点包装成组件,在对应的生命周期中分别取缓存、回收、复用。
import { ItemData } from '../model/ItemData'; import { NodeItem, NodePool } from '../utils/BuilderNodePool'; // [Start node3] // The list item placeholder component with NodeContainer @Component export struct DiffListItemNode { @State type: string = ''; @State item: ItemData = new ItemData('', 0); @State itemHeight: number = 0; @State builder: WrappedBuilder<ESObject> | null = null; private nodeItem: NodeItem = new NodeItem(); 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); } build() { NodeContainer(this.nodeItem) } } -
封装列表项的界面视图组件,使用listItemBuilder函数对外export该组件。
import { ItemData } from '../model/ItemData'; // [Start node4] // export the list item component @Builder export function listItemBuilder(data: ESObject) { DiffListItemView({ item: data.item }) } // The list item view component @Component export struct DiffListItemView { // [StartExclude node4] @Prop item: ItemData; // [EndExclude node4] aboutToReuse(params: ESObject): void { this.item = params.item; } build() { Row() { // [StartExclude node4] Column() { Text(this.item.title) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(Color.Black) .maxLines(2) .lineHeight(22) .opacity(0.9) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.item.tail) .fontSize(12) .opacity(0.4) .fontWeight(FontWeight.Regular) .margin({ top: 18 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Image(this.item.pics[1]) .width(96) .height(78) .borderRadius(8) .margin({ left: 12 }) // [EndExclude node4] } // [StartExclude node4] .alignItems(VerticalAlign.Top) .padding({ top: 16, bottom: 12, left: 16, right: 16 }) .margin({ top: 12, left: 16, right: 16 }) .borderRadius(12) .backgroundColor(Color.White) // [EndExclude node4] } } // [End node4] -
在列表的LazyForEach()中,将步骤4的实际列表项视图wrapBuilder后作为参数传递给步骤3封装的占位组件,实现复用组件的布局。
import { DiffListItemNode } from './DiffListItemNode'; import { ItemData } from '../model/ItemData'; import { genMockItemData } from '../common/MockData'; import { listItemBuilder } from './DiffListItemView'; import { ItemDataSource } from '../model/ItemDataSource'; const REUSE_VIEW_TYPE_ITEM: string = 'reuse_type_'; // [Start node5] // wrapBuilder the list item view component export const listItemWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(listItemBuilder); // The list component in the swiper @Component export struct TabContentView { // [StartExclude node5] minSize: number = 80; maxSize: number = 180; fontSize: number = 24; @State index: number = 0; dataSource: ItemDataSource = new ItemDataSource(); aboutToAppear(): void { this.dataSource.addItems(genMockItemData(1000)); } fillNewData(item: ItemData) { this.dataSource.addLastItem(item); } // [EndExclude node5] build() { List() { LazyForEach(this.dataSource, (item: ItemData) => { DiffListItemNode({ type: REUSE_VIEW_TYPE_ITEM, item: item, builder: listItemWrapper }) }, (item: ItemData) => item.id.toString()) } // [StartExclude node5] .padding({ bottom: 24 }) .contentEndOffset(24) .width('100%') .height('100%') .cachedCount(1) .id("id_custom_reusable_pool_list") // [EndExclude node5] } } // [End node5]
使用onIdle()预创建组件
在当前场景下,首次进入页面可能耗时较高,因为在第一次进入时,自定义组件复用池中没有缓存可以复用,列表项都需要新创建。优化这个问题,可以考虑预创建组件,将组件对象提前放入复用缓存池中。
当组件数量较多,集中预创建本身也耗时较长,容易导致主线程阻塞。ArkUI中提供了onIdle()接口,会返回每一帧帧尾的空闲时间,可以将组件预创建分布到每一帧帧尾的空闲时间中执行,这样预创建过程就被平摊在多个周期里执行,避免集中运行的耗时影响,进而优化应用体验。

注意
需要根据业务准确预估组件预创建耗时,同时将业务逻辑颗粒度拆小,以便能够分到多个onIdle()时机中完成。例如,单个组件预创建耗时在2ms左右,帧尾空闲时间只有1ms,那么就不能在当前帧进行预创建,而是延迟到下一帧中执行。
需要合理控制自定义组件复用池中预创建的数量,否则内存占用较多,可能会影响性能。
- 在NodePool工具类中实现预创建preBuild()方法:新建NodeItem实例,设置builder等属性,执行recycleNode()提前放入缓存池中。
- 继承FrameCallback实现帧回调类,在构造器中传入预创建的数据,并实现onIdle()接口。
- 系统会通过onIdle()回调,将帧尾空闲时间通过参数idleTimeInNano传递出来,可根据单个组件的预创建耗时,设置预创建的剩余空闲时间上限(示例代码假设单个组件预创建耗时最长1ms=1000000ns)。
- 当剩余空闲时间足够创建组件时,在这一帧中进行组件预创建,并不断更新当前帧的剩余空闲时间。
- 若当前帧剩余空闲时间不足以创建组件,通过postFrameCallback()方法,将回调传递到下一帧,继续进行剩余组件的预创建。
- 在进入Swiper+List的页面之前,选择合适的时机执行context.postFrameCallback(),开启IdleCallback帧回调逻辑。
更多推荐



所有评论(0)