鸿蒙(HarmonyOS)应用开发之性能优化实战-组件复用四板斧
在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过组件复用四板斧提升复用性能。第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
概述
在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过组件复用四板斧提升复用性能。
组件复用四板斧:
- 第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
- 第二板斧,优化状态管理,精准控制组件刷新范围,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。
- 第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。
- 第四板斧,不要使用函数/方法作为复用组件的入参,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。
组件复用原理机制
-
如上图①中,ListItem N-1滑出可视区域即将销毁时,如果标记了@Reusable,就会进入这个自定义组件所在父组件的复用缓存区。需注意在自定义组件首次显示时,不会触发组件复用。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其是该复用组件具有相同的布局结构,仅有某些数据差异时,通过组件复用可以提高列表页面的加载速度和响应速度。
-
如上图②中,复用缓存池是一个Map套Array的数据结构,以reuseId为key,具有相同reuseId的组件在同一个Array中。如未设置reuseId,则reuseId默认是自定义组件的名字。
-
如上图③中,发生复用行为时,会自动递归调用复用池中取出的自定义组件的aboutToReuse回调,应用可以在这个时候刷新数据。
第一板斧,减少组件复用的嵌套层级
在组件复用场景下,过深的自定义组件的嵌套会增加组件复用的使用难度,比如需要逐个实现所有嵌套组件中aboutToReuse回调实现数据更新;因此推荐优先使用@Builder替代自定义组件,减少嵌套层级,利于维护切能提升页面加载速度。正反例如下:
反例:
@Entry
@Component
struct ReduceLevel {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 30; index++) {
this.data.pushData(index.toString())
}
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
//反例 使用自定义组件
ComponentA({ desc: item })
}
}, (item: string) => item)
}
}
}
}
@Reusable
@Component
struct ComponentA {
@State desc: string = '';
aboutToReuse(params: ESObject): void {
this.desc = params.desc as string;
}
build() {
// 在复用组件中嵌套使用自定义组件
ComponentB({ desc: this.desc })
}
}
@Component
struct ComponentB {
@State desc: string = '';
// 嵌套的组件中也需要实现aboutToReuse来进行UI的刷新
aboutToReuse(params: ESObject): void {
this.desc = params.desc as string;
}
build() {
Column() {
Text('子组件' + this.desc)
.fontSize(30)
.fontWeight(30)
}
}
}
上述反例的操作中,在复用的自定义组件中嵌套了新的自定义组件。ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和和RenderNod渲染e的耗时。且嵌套的自定义组件ComponentB,也需要实现aboutToReuse来进行数据的刷新。
正例:
@Entry
@Component
struct ReduceLevel {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 30; index++) {
this.data.pushData(index.toString())
}
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 正例
ChildComponent({ desc: item })
}
}, (item: string) => item)
}
}
}
}
// 正例 使用组件复用
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
}
build() {
Column() {
// 使用@Builder,可以减少自定义组件创建和渲染的耗时
ChildComponentBuilder({ paramA: this.desc })
}
}
}
class Temp {
paramA: string = '';
}
@Builder
function ChildComponentBuilder($$: Temp) {
Column() {
// 此处使用`${}`来进行按引用传递,让@Builder感知到数据变化,进行UI刷新
Text(子组件 + ${$$.paramA})
.fontSize(30)
.fontWeight(30)
}
}
上述正例的操作中,在复用的自定义组件中用@Builder来代替了自定义组件。避免了CustomNode节点创建和RenderNode渲染的耗时。
第二板斧,优化状态管理,精准控制组件刷新范围使用
1.使用attributeModifier精准控制组件属性的刷新,避免组件不必要的属性刷新
复用场景常用在高频的刷新场景,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。正反例如下:
反例:
@Entry
@Component
struct PreciseRefreshing {
@State mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表
build() {
Column() {
List() {
LazyForEach(this.mainContentData, (item: VideoDataType) => {
ListItem() {
MyComponent({ authorName: item.authorName, fontSize: item.fontWeight })
}
}, (item: VideoDataType) => item.desc + item.fontWeight)
}
}
}
}
@Reusable
@Component
export struct MyComponent {
...
@State fontSize: number = 0;
aboutToReuse(params: ESObject): void {
this.authorName = params.authorName;
this.fontSize = params.fontSize;
}
build() {
RelativeContainer() {
Text(this.videoDesc)
.textAlign(TextAlign.Center)
.fontStyle(FontStyle.Normal)
.fontColor(Color.Pink)
.id('videoName')
.margin({ left: 10 })
.fontWeight(30)
.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
// 此处使用属性直接进行刷新,会造成Text所有属性都刷新
.fontSize(this.fontSize)
}
.width('100%')
.height(100)
}
}
上述反例的操作中,通过aboutToReuse对fontSize状态变量更新,进而导致组件的全部属性进行刷新,造成不必要的耗时。可以考虑对需要更新的组件的属性,进行精准刷新,避免不必要的重绘和渲染。
正例:
export class MyTextModifier implements AttributeModifier<TextAttribute> {
private fontSize: number = 30;
constructor() {
}
setFontSize(instance: TextAttribute,fontSize: number) {
instance.fontSize = fontSize;
return this;
}
applyNormalAttribute(instance: TextAttribute): void {
instance.textAlign(TextAlign.Center)
instance.fontStyle(FontStyle.Normal)
instance.fontColor(Color.Pink)
instance.id('videoName')
instance.margin({ left: 10 })
instance.fontWeight(30)
instance.fontSize(10)
instance.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
}
}
@Entry
@Component
struct PreciseRefreshing {
@State mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表
build() {
Column() {
List() {
LazyForEach(this.mainContentData, (item: VideoDataType) => {
ListItem() {
MyComponent({... fontSize: item.fontWeight })
}
}, (item: VideoDataType) => item.desc + item.fontWeight)
}
}
}
}
@Reusable
@Component
export struct MyComponent {
...
@State fontSize: number = 0;
textModifier:MyTextModifier=new MyTextModifier();
aboutToReuse(params: ESObject): void {
...
this.fontSize = params.fontSize;
this.textModifier.setFontSize(this.textModifier,this.fontSize)
}
build() {
RelativeContainer() {
...
Text(this.videoDesc)
// 采用attributeModifier来对需要更新的fontSize属性进行精准刷新,避免不必要的属性刷新。
.attributeModifier(this.textModifier)
...
}
}
}
上述正例的操作中,通过attributeModifier属性来对text组件需要刷新的fontSize属性进行精准刷新,避免text其它不需要更改的属性的刷新。
2.使用@Link/@ObjectLink替代@Prop减少深拷贝,提升组件创建速度
在父子组件数据同步时,如果仅仅是需要父组件向子组件同步数据,不存在修改子组件的数据变化不同步给父组件的需求。建议使用@Link/@ObjectLink替代@Prop,@Prop在装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。正反例如下:
反例:
@Component
struct ChildComponent {
@Prop message: string;
build() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct FatherComponent {
@State message: string = 'Hello World';
build() {
Column() {
ChildComponent({ message: this.message })
}
}
}
上述反例的操作中,父子组件之间的数据同步用了@Prop来进行,每个@Prop装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。
正例:
@Component
struct ChildComponent {
@Link message: string;
build() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct FatherComponent {
@State message: string = 'Hello World';
build() {
Column() {
ChildComponent({ message: this.message })
}
.width('100%')
.height('100%')
}
}
上述正例的操作中,父子组件之间的数据同步用了@Link来进行,子组件@Link包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。
第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成
在自定义组件复用的场景中,如果使用if/else条件语句来控制布局的结构,会导致在不同逻辑创建不同布局结构嵌套的组件,从而造成组件树结构的不同。此时我们应该使用reuseId来区分不同结构的组件,确保系统能够根据reuseId缓存各种结构的组件,提升复用性能。正反例如下:
反例:
@Entry
@Component
struct ReuseID {
...
build() {
Column() {
List({ scroller: this.scroller }) {
LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
ListItem() {
Button({ type: ButtonType.Normal }) {
Row() {
if (chatInfo['isPublicChat']) {
PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
} else {
ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
.onClick(() => {
const sessionType = (chatInfo as ChatSessionEntity).sessionType
autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
imLogic.chat.chatSort()
})
}
}.padding({ left: 16, right: 16 })
}
.type(ButtonType.Normal)
.width('100%')
.height('100%')
.backgroundColor('#fff')
.borderRadius(0)
}
.height(72)
.swipeAction({
end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
})
}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
)
}
.cachedCount(3)
.backgroundColor('#fff')
.onScrollIndex(startIndex => {
this.listStartIndex = startIndex;
})
.width('100%')
.height('100%')
}
}
}
@Reusable
@Component
struct PublicChatItem {
...
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
...
}
}
@Reusable
@Component
struct ChatItem {
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
...
}
}
上述反例的操作中,通过if else来控制组件树走不同的分支,分别复用PublicChatItem组件和ChatItem组件。导致更新if分支时仍然走删除重创的逻辑。考虑采用根据不同的分支设置不同的reuseId来提高复用的性能。
正例:
@Entry
@Component
struct ReuseID {
...
build() {
Column() {
List({ scroller: this.scroller }) {
LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
ListItem() {
// 使用reuseId进行组件复用的控制
InnerRecentChat({ chatInfo: chatInfo }).reuseId(this.lazyChatList.getReuseIdByIndex(index))
}
.height(72)
.swipeAction({
end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
})
}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
)
}
.cachedCount(3)
.backgroundColor('#fff')
.onScrollIndex(startIndex => {
this.listStartIndex = startIndex;
})
.width('100%')
.height('100%')
}
}
}
@Reusable
@Component
struct InnerRecentChat {
...
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
Button({ type: ButtonType.Normal }) {
Row() {
if (this.chatInfo['isPublicChat']) {
PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
} else {
ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
.onClick(() => {
const sessionType = (chatInfo as ChatSessionEntity).sessionType
autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
imLogic.chat.chatSort()
})
}
}.padding({ left: 16, right: 16 })
}
.type(ButtonType.Normal)
.width('100%')
.height('100%')
.backgroundColor('#fff')
.borderRadius(0)
}
}
class MtDataSource extends BasicDataSource{
private chatList:Array<ChatSessionEntity|IChat.PublicChat>=[];
private reuseIds:Array<string>=[];
public totalCount():number{
return this.chatList.length;
}
public set (list:Array<ChatSessionEntity|IChat.PublicChat>){
this.chatList=list;
this.reuseIds=list.map((value:ChatSessionEntity|IChat.PublicChat)=>{
if (value['isPublicChat']) {
return "public";
}
else {
if ((value as ChatSessionEntity).target?.isEmployeeEntity()) {
return "employee"
}else {
return "group"
}
}
})
this.notifyDataReload();
}
pubilc getReuseIdByIndex(index:number):string{
return this.reuseIds
}
}
上述正例的操作中,通过reuseId来标识需要复用的组件,省去走if else删除重创的逻辑,提高组件复用的效率和性能。
第四板斧,避免使用函数/方法作为复用组件创建时的入参
由于在组件复用的场景下,每次复用都需要重新创建组件关联的数据对象,导致重复执行入参中的函数来获取入参结果。如果函数中存在耗时操作,会严重影响性能。正反例如下:
【反例】
// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
@State sum: number = 0;
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
this.sum = params.sum as number;
}
build() {
Column() {
Text('子组件' + this.desc)
.fontSize(30)
.fontWeight(30)
Text('结果' + this.sum)
.fontSize(30)
.fontWeight(30)
}
}
}
@Entry
@Component
struct Reuse {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 20; index++) {
this.data.pushData(index.toString())
}
}
// 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作
count(): number {
let temp: number = 0;
for (let index = 0; index < 10000; index++) {
temp += index;
}
return temp;
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 此处sum参数是函数获取的,实际开发场景无法预料该函数可能出现的耗时操作,每次进行组件复用都会重复触发此函数的调用
ChildComponent({ desc: item, sum: this.count() })
}
.width('100%')
.height(100)
}, (item: string) => item)
}
}
}
}
上述反例的操作中,复用的子组件参数sum是通过耗时函数生成。该函数在每次组件复用时都需要执行,会造成性能问题,甚至是列表滑动过程中的卡顿丢帧现象。
【正例】
// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
@State sum: number = 0;
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
this.sum = params.sum as number;
}
build() {
Column() {
Text('子组件' + this.desc)
.fontSize(30)
.fontWeight(30)
Text('结果' + this.sum)
.fontSize(30)
.fontWeight(30)
}
}
}
@Entry
@Component
struct Reuse {
private data: BasicDateSource = new BasicDateSource();
@State sum: number = 0;
aboutToAppear(): void {
for (let index = 0; index < 20; index++) {
this.data.pushData(index.toString())
}
// 执行该异步函数
this.count();
}
// 模拟耗时操作逻辑
async count() {
let temp: number = 0;
for (let index = 0; index < 10000; index++) {
temp += index;
}
// 将结果放入状态变量中
this.sum = temp;
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 子组件的传参通过状态变量进行
ChildComponent({ desc: item, sum: this.sum })
}
.width('100%')
.height(100)
}, (item: string) => item)
}
}
}
}
上述正例的操作中,通过耗时函数count生成的结果不变,可以将其放到页面初始渲染时执行一次,将结果赋值给this.sum。在复用组件的参数传递时,通过this.sum来进行。
码牛课堂也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线。大家可以进行参考学习:https://qr21.cn/FV7h05
①全方位,更合理的学习路径:
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!②多层次,更多的鸿蒙原生应用:
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。③实战化,更贴合企业需求的技术点:
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。
为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05
《鸿蒙开发学习手册》:https://qr21.cn/FV7h05
如何快速入门:
- 基本概念
- 构建第一个ArkTS应用
- ……
开发基础知识:https://qr21.cn/FV7h05
- 应用基础知识
- 配置文件
- 应用数据管理
- 应用安全管理
- 应用隐私保护
- 三方应用调用管控机制
- 资源分类与访问
- 学习ArkTS语言
- ……
基于ArkTS 开发:https://qr21.cn/FV7h05
- Ability开发
- UI开发
- 公共事件与通知
- 窗口管理
- 媒体
- 安全
- 网络与链接
- 电话服务
- 数据管理
- 后台任务(Background Task)管理
- 设备管理
- 设备使用信息统计
- DFX
- 国际化开发
- 折叠屏系列
- ……
鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05
大厂鸿蒙面试题::https://qr18.cn/F781PH
鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH
1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向
更多推荐
所有评论(0)