概述
若开发者的应用中存在以下场景,并成为UI线程的帧率瓶颈,应该考虑使用组件复用机制提升应用性能:
●列表滚动(本例中的场景):当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
●动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
●地图渲染:在地图渲染这种场景下,频繁创建和销毁数据项的视图可能导致性能问题。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
●HarmonyOS应用框架提供了组件复用能力:可复用组件从组件树上移除时,会进入到一个回收缓存区,后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。
本文会介绍如何使用组件复用机制提升应用帧率。
组件复用原理与使用
原理介绍
组件复用机制如下:
1.标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。
2.当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节点。
3.找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。
图1 组件复用原理图
说明
需要注意的是,虽然这里是使用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。
组件复用优化方法
减少组件复用的嵌套层级
在组件复用场景下,过深的自定义组件的嵌套会增加组件复用的使用难度,比如需要逐个实现所有嵌套组件中aboutToReuse回调实现数据更新;因此推荐优先使用@Builder替代自定义组件,减少嵌套层级,利于维护切能提升页面加载速度。正反例如下:
反例:
@Entry
@Component
struct lessEmbeddedComponent {
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMomentNoBuilder({moment: moment})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMomentNoBuilder {
@Prop moment: FriendMoment;
// 无需对@Prop修饰的变量进行aboutToReuse赋值,因为这些变量是由父组件传递给子组件的。如果在子组件中重新赋值这些变量,会导致重用的组件的内容重新触发状态刷新,从而降低组件的复用性能。
build() {
...
// 在复用组件中嵌套使用自定义组件
Row() {
InteractiveButton({
imageStr: $r('app.media.ic_share'),
text: $r('app.string.friendMomentsPage_share')
})
Blank()
InteractiveButton({
imageStr: $r('app.media.ic_thumbsup'),
text: $r('app.string.friendMomentsPage_thumbsup')
})
Blank()
InteractiveButton({
imageStr: $r('app.media.ic_message'),
text: $r('app.string.friendMomentsPage_message')
})
}
...
}
}
@Component
export struct InteractiveButton {
@State imageStr: ResourceStr;
@State text: ResourceStr;
// 嵌套的组件中也需要实现aboutToReuse来进行UI的刷新
aboutToReuse(params: Record<string, Object>): void {
this.imageStr = params.imageStr as ResourceStr;
this.text = params.text as ResourceStr;
}
build() {
Row() {
Image(this.imageStr)
Text(this.text)
}
.alignItems(VerticalAlign.Center)
}
}
上述反例的操作中,在复用的自定义组件中嵌套了新的自定义组件。ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和RenderNod渲染的耗时。且嵌套的自定义组件InteractiveButton,也需要实现aboutToReuse来进行数据的刷新。
正例:
@Entry
@Component
struct lessEmbeddedComponent {
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConsta