鸿蒙HarmonyOS NEXT开发:优化用户界面性能——组件复用(@Reusable装饰器)

embedded/2025/2/13 18:54:14/

文章目录

      • 一、概述
      • 二、原理介绍
      • 三、使用规则
      • 四、复用类型详解
        • 1、标准型
        • 2、有限变化型
          • 2.1、类型1和类型2布局不同,业务逻辑不同
          • 2.2、类型1和类型2布局不同,但是很多业务逻辑公用
        • 3、组合型
        • 4、全局型
        • 5、嵌套型

一、概述

组件复用是优化用户界面性能,提升应用流畅度的一种重要手段,通过复用已存在的组件节点而非创建新的节点,从而确保UI线程的流畅性与响应速度。

组件复用针对的是自定义组件,只要发生了相同自定义组件销毁和再创建的场景,都可以使用组件复用,例如滑动列表场景,会出现大量重复布局的创建,使用组件复用可以大幅度降低了因频繁创建与销毁组件带来的性能损耗。

然而,面对复杂的业务场景或者布局嵌套的场景下,组件复用使用不当,可能会导致复用失效或者性能提升不能最大化。例如列表中存在多种布局形态的列表项,无法直接复用。

本文基于对常见的布局类型进行划分,通过合理使用组件复用方式,帮助开发者更好的理解和实施组件复用策略以优化应用性能。

二、原理介绍

组件复用机制如下:

组件复用原理图

在这里插入图片描述

1、@Reusable表示组件可以被复用,结合LazyForEach懒加载一起使用,可以进一步解决列表滑动场景的瓶颈问题,提供滑动场景下高性能创建组件的方式来提升滑动帧率。

2、CustomNode是一种自定义的虚拟节点,它可以用来缓存列表中的某些内容,以提高性能和减少不必要的渲染。通过使用CustomNode,可以实现只渲染当前可见区域内的数据项,将未显示的数据项缓存起来,从而减少渲染的数量,提高性能。

3、RecycleManager是一种用于优化资源利用的回收管理器。当一个数据项滚出屏幕时,不会立即销毁对应的视图对象,而是将该视图对象放入复用池中。当新的数据项需要在屏幕上展示时,RecycleManager会从复用池中取出一个已经存在的视图对象,并将新的数据绑定到该视图上,从而避免频繁的创建和销毁过程。通过使用RecycleManager,可以大大减少创建和销毁视图的次数,提高列表的滚动流畅度和性能表现。

4、CachedRecycleNodes是CustomNode的一个集合,常是用于存储被回收的CustomNode对象,以便在需要时进行复用。

说明
需要注意的是,虽然这里是使用List组件进行举例,但是不代表组件复用只能用在滚动容器里,只要是发生了相同自定义组件销毁和再创建的场景,都可以使用组件复用

三、使用规则

组件复用的示例代码如下:

// xxx.ets
export class Message {value: string | undefined;constructor(value: string) {this.value = value}
}@Entry
@Component
struct Index {@State switch: boolean = truebuild() {Column() {Button('Hello World').fontSize(50).fontWeight(FontWeight.Bold).onClick(() => {this.switch = !this.switch})if (this.switch) {Child({ message: new Message('Child') })// 如果只有一个复用的组件,可以不用设置reuseId.reuseId('Child')}}.height("100%").width('100%')}
}@Reusable
@Component
struct Child {@State message: Message = new Message('AboutToReuse');aboutToReuse(params: Record<string, ESObject>) {console.info("Recycle Child")this.message = params.message as Message}build() {Column() {Text(this.message.value).fontSize(20)}.borderWidth(2).height(100)}
}

1.@Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力。

2.aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse。

3.reuseId:用于标记自定义组件复用组,当组件回收复用时,复用框架将根据组件的reuseId来划分组件的复用组。如果只有一个复用的组件,可以不用设置reuseId。

四、复用类型详解

组件复用基于不同的布局效果和复用的诉求,可以分为以下五种类型。

表1 组件复用类型说明

复用类型描述复用思路
标准型复用组件之间布局完全相同标准复用
有限变化型复用组件之间布局有所不同,但是类型有限使用reuseId或者独立成不同自定义组件
组合型复用组件之间布局有不同,情况非常多,但是拥有共同的子组件将复用组件改为@Builder,让内部子组件相互之间复用
全局型组件可在不同的父组件中复用,并且不适合使用@Builder使用BuilderNode自定义复用组件池,在整个应用中自由流转
嵌套型复用组件的子组件的子组件存在差异采用化归思想将嵌套问题转化为上面四种标准类型来解决

下面将以滑动列表的场景为例介绍5种复用类型的使用场景,为了方便描述,下文将需要复用的自定义组件如ListItem的内容组件,叫做复用组件,将其下层的自定义组件叫做子组件、复用组件上层的自定义组件叫做父组件。为了更直观,下面每一种复用类型都会通过简易的图形展示组件的布局方式,并且为了便于分辨,布局相同的子组件使用同一种形状图形表示。

1、标准型

在这里插入图片描述

这是一个标准的组件复用场景,一个滚动容器内的复用组件布局相同,只有数据不同,这种类型的组件复用可以直接参考资料组件复用。其缓存池如下,因为该场景只有一个复用组件,所以在缓存中只有一个复用组件list:

在这里插入图片描述

典型场景如下,列表Item布局基本完全相同。

在这里插入图片描述

标准型组件复用的示例代码如下:

@Entry
@Component
struct ReuseType1 {// ...build() {Column() {List() {LazyForEach(this.dataSource, (item: string) => {ListItem() {CardView({ item: item })}}, (item: string) => item)}}}
}// 复用组件
@Reusable
@Component
export struct CardView {@State item: string = '';aboutToReuse(params: Record<string, Object>): void {this.item = params.item as string;}// ...
}
2、有限变化型

在这里插入图片描述

如上图所示,有限变化型指的是父组件内存在多个类型的复用单元,这些类型的单元布局有所不同,根据业务逻辑的差异可以分为以下两种情况:

  • 类型1和类型2布局不同,业务逻辑不同:这种情况可以使用两个不同的自定义组件进行复用。

  • 类型1和类型2布局不同,但是很多业务逻辑公用:这种情况为了复用公用的逻辑代码,减少代码冗余,可以给同一个组件设置不同的reuseId来进行复用。

下面将分别介绍这两种场景下的组件复用方法。

2.1、类型1和类型2布局不同,业务逻辑不同

在这里插入图片描述

类型1和类型2布局不同,业务逻辑不同:因为两种类型的组件布局会对应应用不同的业务处理逻辑,建议将两种类型的组件分别使用两个不同的自定义组件,分别进行复用。给复用组件1和复用组件2设置不同的reuseId,此时组件复用池内的状态如下图所示,复用组件1和复用组件2处于不同的复用list中。

例如下面的列表场景,列表项布局差距比较大,有多图片的列表项,有单图片的列表项:

在这里插入图片描述

实现方式可参考以下示例代码:

@Entry
@Component
struct ReuseType2A {// ...build() {Column() {List() {LazyForEach(this.dataSource, (item: number) => {ListItem() {if (item % 2 === 0) { // 模拟业务条件判断SinglePicture({ item: item }) // 渲染单图片列表项} else {MultiPicture({ item: item }) // 渲染多图片列表项}}}, (item: number) => item + '')}}}
}// 复用组件1
@Reusable
@Component
struct SinglePicture {// ...
}// 复用组件2
@Reusable
@Component
struct MultiPicture {// ...
}
2.2、类型1和类型2布局不同,但是很多业务逻辑公用

在这里插入图片描述

类型1和类型2布局不同,但是很多业务逻辑公用:在这种情况下,如果将组件分为两个自定义组件进行复用,会存在代码冗余问题。根据布局的差异,可以给同一个组件设置不同的reuseId从而复用同一个组件,达到逻辑代码的复用。

根据组件复用原理与使用可知,复用组件是依据reuseId来区分复用缓存池的,而自定义组件的名称就是默认的reuseId。因此,为复用组件显式设置两个不同的reuseId与使用两个自定义组件进行复用,对于 ArkUI 而言,复用逻辑完全相同,复用池也一样,只不过复用池中复用组件的list以reuseId作为标识。

例如下面这个场景,布局差异比较小,业务逻辑一样都是跳转到页面详情。这种情况复用同一个组件,只需要使用if/else条件语句来控制布局的结构,就可以实现,同时可以复用跳转详情的公用逻辑代码。但是这样会导致在不同逻辑会反复去修改布局,造成性能损耗。开发者可以根据不同的条件,设置不同的reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能。

在这里插入图片描述

实现方式可以参考以下示例:

@Entry
@Component
struct ReuseType2B {// ...build() {Column() {List() {LazyForEach(this.dataSource, (item: MemoInfo) => {ListItem() {MemoItem({ memoItem: item })// 使用reuseId进行组件复用的控制.reuseId((item.imageSrc !== '') ? 'withImage' : 'noImage')}}, (item: MemoInfo) => JSON.stringify(item))}}}
}@Reusable
@Component
export default struct MemoItem {@State memoItem: MemoInfo = MEMO_DATA[0];aboutToReuse(params: Record<string, Object>) {this.memoItem = params.memoItem as MemoInfo;}build() {Row() {// ...if (this.memoItem.imageSrc !== '') {Image($r(this.memoItem.imageSrc)).width(90).aspectRatio(1).borderRadius(10)}}// ...}
}
3、组合型

在这里插入图片描述

这种类型中复用组件之间存在不同,并且情况比较多,但拥有共同的子组件。如果使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,不同复用组件(组件名不同或者reuseld不同)之间相同子组件无法复用,因为它们在缓存池的不同List中。

对此可以将复用组件转变为@Builder函数,使复用组件内部共同的子组件的缓存池在父组件上共享,此时组件复用池内的状态如下图所示。

典型场景如下图,这个列表的Item有多种组合方式。但是每个Item上面和下面的布局是一样的,中间部分的布局有所不同,有单一图片、视频、九宫等等。

在这里插入图片描述

示例代码如下,列举了单一图片、视频和九宫格图片三种类型的列表项目,使用Builder函数后将子组件组合成三种不同的类型,使内部共同的子组件就处于同一个父组件FriendsMomentsPage下。对这些子组件使用组件复用时,他们的缓存池也会在父组件上共享,节省组件创建时的消耗。

@Entry
@Component
struct ReuseType3 {// ...@BuilderitemBuilderSingleImage(item: FriendMoment) { // 单大图列表项// ...}@BuilderitemBuilderGrid(item: FriendMoment) { // 九宫格列表项// ...}@BuilderitemBuilderVideo(item: FriendMoment) { // 视频列表项// ...}build() {Column() {List() {LazyForEach(this.momentDataSource, (item: FriendMoment) => {ListItem() {if (item.type === 1) { // 根据不同类型,使用不同的组合this.itemBuilderSingleImage(item);} else if (item.type === 2) {this.itemBuilderGrid(item);} else if (item.type === 3) {this.itemBuilderVideo(item);} else {// ...}}}, (moment: FriendMoment) => JSON.stringify(moment))}}}
}@Reusable
@Component
struct ItemTop {// ...
}@Reusable
@Component
struct ItemBottom {// ...
}@Reusable
@Component
struct MiddleSingleImage {// ...
}@Reusable
@Component
struct MiddleGrid {// ...
}@Reusable
@Component
struct MiddleVideo {// ...
}
4、全局型

在这里插入图片描述

默认的组件复用行为,是将子组件放在父组件的缓存池里,受到这个限制,不同父组件中的相同子组件无法复用,推荐的解决方案是将父组件改为builder函数,让子组件共享组件复用池,但是由于在一些应用场景下,父组件承载了复杂的带状态的业务逻辑,而builder是无状态的,修改会导致难以维护,因此开发者可以使用BuilderNode自行管理组件复用池。

有时候应用在多个tab页之间切换,tab页之间结构类似,需要在tab页之间复用组件,提升页面切换性能。或者有些应用在组合型场景下,由于复用组件内部含有较多带状态的业务逻辑,所以不适合改为Builder函数。

针对这种类型的组件复用场景,可以通过BuilderNode自定义缓存池,将要复用的组件封装在BuilderNode中,将BuilderNode的NodeController作为复用的最小单元,自行管理复用池。

5、嵌套型

在这里插入图片描述

嵌套型是指复用组件的子组件的子组件之间存在差异的复用场景。如上图所示,列表项复用组件1之间的差异是子组件B的子组件不一样,有子组件C、D、E三种。这种情况可以运行化归的思想,将复杂的问题转化为已知的、简单的问题

嵌套型实际上是上面四种类型的组合,以上图为例,可以通过有限变化型的方案,将子组件B变为子组件B1/B2/B3,这样问题就变成了一个标准的有限变化型,A/B1/C、A/B2/D、A/B3/E会分别作为一个组合进行复用,复用池如下:
在这里插入图片描述

下面列举一个简单的示例介绍嵌套型的使用:

@Entry
@Component
struct ReuseType5A {// ...build() {Column() {List() {LazyForEach(this.dataSource, (item: number) => {ListItem() {if (item % 2 === 0) { // 模拟类型一的条件ReusableComponent({ item: item }).reuseId('type1')} else if (item % 3 === 0) { // 模拟类型二的条件ReusableComponent({ item: item }).reuseId('type2')} else { // 模拟类型三的条件ReusableComponent({ item: item }).reuseId('type3')}}}, (item: number) => item.toString())}}}
}// 复用组件
@Reusable
@Component
struct ReusableComponent {@State item: number = 0;build() {Column() {ComponentA()if (this.item % 2 === 0) {ComponentB1()} else if (this.item % 3 === 0) {ComponentB2()} else {ComponentB3()}}}
}@Component
struct ComponentA {// ...
}@Component
struct ComponentB1 {build() {Column() {ComponentC()}}
}@Component
struct ComponentB2 {build() {Column() {ComponentD()}}
}@Component
struct ComponentB3 {build() {Column() {ComponentE()}}
}@Component
struct ComponentC {// ...
}@Component
struct ComponentD {// ...
}@Component
struct ComponentE {// ...
}

http://www.ppmy.cn/embedded/161938.html

相关文章

Eclipse 插件开发相关概念

整理了Eclipse插件开发的概念&#xff0c;用于熟悉入门 SWT&#xff08;Standard Widget Toolkit&#xff09;标准图形工具箱 Java开发的GUI程序技术&#xff0c;由Eclipse开发&#xff0c;相比AWT、Swing更美观&#xff1b;对于目标平台上已经有的控件&#xff0c;SWT会直接使…

【JavaWeb10】服务器渲染技术 --- JSP

文章目录 &#x1f30d;一. JSP❄️1.JSP介绍❄️2.JSP 运行原理❄️3.page 指令(常用的)❄️ 4.JSP 三种常用脚本1.声明脚本2.表达式脚本3.代码脚本 ❄️5.JSP 内置对象❄️6.JSP 域对象 &#x1f30d;二. EL❄️1.EL 表达式介绍❄️2.EL 运算操作❄️3.EL 的 11 个隐含对象 &…

day50 第十一章:图论part01

ACM模式&#xff0c;自己控制输入输出 图论理论基础 连通性&#xff1a; 连通图&#xff08;无向&#xff09;&#xff0c;强连通图&#xff08;有向&#xff09;----- 任意两个节点之间都可相互到达 连通分量&#xff08;极大连通子图&#xff09;&#xff0c;强连通分量 图的…

springboot配置https

注意&#xff1a; 此配置只能本地环境或测试环境使用&#xff0c;生产环境使用https&#xff0c;应该配置nginx&#xff01;请参考&#xff1a;使用certbot给nginx配置https-CSDN博客 1. 生成证书 使用JDK的keytool命令生成证书 注意&#xff1a;JDK版本需要和项目的JDK版本一…

vue2 多页面pdf预览

使用pdfjs-dist预览pdf&#xff0c;实现预加载&#xff0c;滚动条翻页。pdfjs的版本很重要&#xff0c;换了好多版本&#xff0c;终于有一个能用的 node 20.18.1 "pdfjs-dist": "^2.2.228", vue页面代码如下 <template><div v-loading"loa…

Eclipse JSP/Servlet 深入解析

Eclipse JSP/Servlet 深入解析 引言 随着互联网的快速发展,Java Web开发技术逐渐成为企业级应用开发的主流。在Java Web开发中,JSP(JavaServer Pages)和Servlet是两个核心组件,它们共同构成了Java Web应用程序的基础。本文将深入解析Eclipse平台下的JSP/Servlet技术,帮…

Ubuntu 上安装 Java 1.8

在 Ubuntu 上安装 Java 1.8&#xff08;Java 8&#xff09;可以通过以下步骤完成&#xff1a; 方法 1&#xff1a;通过 APT 包管理器安装 OpenJDK 8 这是最常见和推荐的方法。 更新包管理器 sudo apt update sudo apt upgrade -y安装 OpenJDK 8 sudo apt install openjdk-8-jd…

力扣动态规划-26【算法学习day.120】

前言 ###我做这类文章一个重要的目的还是记录自己的学习过程&#xff0c;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&#xff01;&#xff01;&#xff01; 习题 1.目标和 题目链接:494. 目标和 -…