文章目录
- 一、概述
- 二、原理介绍
- 三、使用规则
- 四、复用类型详解
- 1、标准型
- 2、有限变化型
- 2.1、类型1和类型2布局不同,业务逻辑不同
- 2.2、类型1和类型2布局不同,但是很多业务逻辑公用
- 3、组合型
- 4、全局型
- 5、嵌套型
一、概述
组件复用是优化用户界面性能,提升应用流畅度的一种重要手段,通过复用已存在的组件节点而非创建新的节点,从而确保UI线程的流畅性与响应速度。
组件复用针对的是自定义组件,只要发生了相同自定义组件销毁和再创建的场景,都可以使用组件复用,例如滑动列表场景,会出现大量重复布局的创建,使用组件复用可以大幅度降低了因频繁创建与销毁组件带来的性能损耗。
然而,面对复杂的业务场景或者布局嵌套的场景下,组件复用使用不当,可能会导致复用失效或者性能提升不能最大化。例如列表中存在多种布局形态的列表项,无法直接复用。
本文基于对常见的布局类型进行划分,通过合理使用组件复用方式,帮助开发者更好的理解和实施组件复用策略以优化应用性能。
二、原理介绍
组件复用机制如下:
- 标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。
- 当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节点。
- 找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。
组件复用原理图
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 = true
build() {
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 {
// ...
@Builder
itemBuilderSingleImage(item: FriendMoment) { // 单大图列表项
// ...
}
@Builder
itemBuilderGrid(item: FriendMoment) { // 九宫格列表项
// ...
}
@Builder
itemBuilderVideo(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 {
// ...
}