概述
在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过组件复用提升复用性能。
组件复用四板斧:
- 减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
- 优化状态管理,精准控制组件刷新范围,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。
- 复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。
- 不要使用函数/方法作为复用组件的入参,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。
组件复用原理机制
-
如上图①中,ListItem N-1滑出可视区域即将销毁时,如果标记了@Reusable,就会进入这个自定义组件所在父组件的复用缓存区。需注意在自定义组件首次显示时,不会触发组件复用。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其是该复用组件具有相同的布局结构,仅有某些数据差异时,通过组件复用可以提高列表页面的加载速度和响应速度。
-
如上图②中,复用缓存池是一个Map套Array的数据结构,以reuseId为key,具有相同reuseId的组件在同一个Array中。如未设置reuseId,则reuseId默认是自定义组件的名字。
-
如上图③中,发生复用行为时,会自动递归调用复用池中取出的自定义组件的aboutToReuse回调,应用可以在这个时候刷新数据。
第二板斧,优化状态管理,精准控制组件刷新范围使用
1.使用AttributeUpdater精准控制组件属性的刷新,避免组件不必要的属性刷新
复用场景常用在高频的刷新场景,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。正反例如下:
反例:
@Component
export struct LessEmbeddedComponent {
aboutToAppear(): void {
momentData.getFriendMomentFromRawfile();
}
build() {
Column() {
Text('use nothing')
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMomentNoModifier({ color: moment.color })
.onClick(() => {
console.log(`my id is ${moment.id}`)
})
}
}, (moment: FriendMoment) => moment.id)
}
.width("100%")
.height("100%")
.cachedCount(5)
}
}
}
@Reusable
@Component
export struct OneMomentNoModifier {
@State color: string | number | Resource = "";
aboutToReuse(params: Record<string, Object>): void {
this.color = params.color as number;
}
build() {
Column() {
Text('这是标题')
Text('这是内部文字')
.fontColor(this.color)// 此处使用属性直接进行刷新,会造成Text所有属性都刷新
.textAlign(TextAlign.Center)
.fontStyle(FontStyle.Normal)
.fontSize(13)
.lineHeight(30)
.opacity(0.6)
.margin({ top: 10 })
.fontWeight(30)
.clip(false)
.backgroundBlurStyle(BlurStyle.NONE)
.foregroundBlurStyle(BlurStyle.NONE)
.borderWidth(1)
.borderColor(Color.Pink)
.borderStyle(BorderStyle.Solid)
.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
}
}
}
上述反例的操作中,通过aboutToReuse对fontColor状态变量更新,进而导致组件的全部属性进行刷新,造成不必要的耗时。因此可以考虑对需要更新的组件的属性,进行精准刷新,避免不必要的重绘和渲染。
优化前,由H:ViewPU.viewPropertyHasChanged OneMomentNoModifier color 1
标签可知,OneMomentNoModifier自定义组件下的状态变量color发生变化,与之相关联的子控件数量为1,即有一个子控件发生了标脏,之后Text全部属性会进行了刷新。
此时,H:CustomNode:BuildRecycle
耗时543μs,Create[Text]
耗时为4μs。
正例:
import { AttributeUpdater } from '@ohos.arkui.modifier';
export class MyTextUpdater extends AttributeUpdater<TextAttribute> {
private color: string | number | Resource = "";
constructor(color: string | number | Resource) {
super();
this.color = color
}
initializeModifier(instance: TextAttribute): void {
instance.fontColor(this.color) // 差异化更新
}
}
@Component
export struct UpdaterComponent {
aboutToAppear(): void {
momentData.getFriendMomentFromRawfile();
}
build() {
Column() {
Text('use MyTextUpdater')
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMomentNoModifier({ color: moment.color })
.onClick(() => {
console.log(`my id is ${moment.id}`)
})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(5)
}
}
}
@Reusable
@Component
export struct OneMomentNoModifier {
color: string | number | Resource = "";
textUpdater: MyTextUpdater | null = null;
aboutToAppear(): void {
this.textUpdater = new MyTextUpdater(this.color);
}
aboutToReuse(params: Record<string, Object>): void {
this.color = params.color as string;
this.textUpdater?.attribute?.fontColor(this.color);
}
build() {
Column() {
Text('这是标题')
Text('这是内部文字')
.attributeModifier(this.textUpdater) // 采用attributeUpdater来对需要更新的fontColor属性进行精准刷新,避免不必要的属性刷新。
.textAlign(TextAlign.Center)
.fontStyle(FontStyle.Normal)
.fontSize(13)
.lineHeight(30)
.opacity(0.6)
.margin({ top: 10 })
.fontWeight(30)
.clip(false)
.backgroundBlurStyle(BlurStyle.NONE)
.foregroundBlurStyle(BlurStyle.NONE)
.borderWidth(1)
.borderColor(Color.Pink)
.borderStyle(BorderStyle.Solid)
.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
}
}
}
上述正例的操作中,通过AttributeUpdater来对Text组件需要刷新的属性进行精准刷新,避免Text其它不需要更改的属性的刷新。
优化后,在H:aboutToReuse
标签下没有H:ViewPU.viewPropertyHasChanged
标签,后续也没有Create[Text]
标签。此时,H:CustomNode:BuildRecycle
耗时415μs
优化效果
在正反例中,针对列表滑动场景中,单个列表项中Text组件字体颜色属性的修改,反例中采用了普通组件属性刷新方式实现,正例中采用了AttributeUpdater动态属性设置方式实现。
优化后的H:CustomNode:BuildRecycle OneMomentNoModifier
的耗时,如下表所示:
次数 | 反例:使用@State(单位μs) | 正例:使用AttributeUpdater(单位μs) |
---|---|---|
1 | 357 | 338 |
2 | 903 | 494 |
3 | 543 | 415 |
4 | 543 | 451 |
5 | 692 | 509 |
平均 | 607 | 441 |
不同设备和场景都会对数据有影响,该数据仅供参考。
所以,Trace数据证明,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。
因为示例中仅涉及一个Text组件的属性更新,所以优化时间绝对值较小。如果涉及组件较多,性能提升会更明显。
2.使用@Link/@ObjectLink替代@Prop减少深拷贝,提升组件创建速度
在父子组件数据同步时,如果仅仅是需要父组件向子组件同步数据,不存在修改子组件的数据变化不同步给父组件的需求。建议使用@Link/@ObjectLink替代@Prop,@Prop在装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。正反例如下:
反例:
@Entry
@Component
struct lessEmbeddedComponent {
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({moment: moment})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMoment {
@Prop moment: FriendMoment;
build() {
Column() {
...
Text(`${this.moment.userName}`)
...
}
}
}
export const momentData: FriendMomentsData = new FriendMomentsData();
export class FriendMoment {
id: string;
userName: string;
avatar: string;
text: string;
size: number;
image?: string;
constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) {
this.id = id;
this.userName = userName;
this.avatar = avatar;
this.text = text;
this.size = size;
if (image !== undefined) {
this.image = image;
}
}
}
上述反例的操作中,父子组件之间的数据同步用了@Prop来进行,各@Prop装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。
优化前,子组件在初始化时都在本地拷贝了一份数据,BuildItem耗时7ms175μs。
正例:
@Entry
@Component
struct lessEmbeddedComponent {
@State momentData: FriendMomentsData = new FriendMomentsData();
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({moment: moment})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment;
build() {
Column() {
...
Text(`${this.moment.userName}`)
...
}
}
}
@Observed
export class FriendMoment {
id: string;
userName: string;
avatar: string;
text: string;
size: number;
image?: string;
constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) {
this.id = id;
this.userName = userName;
this.avatar = avatar;
this.text = text;
this.size = size;
if (image !== undefined) {
this.image = image;
}
}
}
上述正例的操作中,父子组件之间的数据同步用了@ObjectLink来进行,子组件@ObjectLink包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。
优化效果
在正反例中,针对列表滑动场景,单个列表项中Text组件字体大小属性的修改,反例中采用了普通组件属性刷新方式实现,正例中采用了attributeModifier动态属性设置方式实现。
优化后,子组件直接同步父组件数据,无需深拷贝,BuildItem耗时缩短为7ms1μs。
所以,Trace数据证明,使用@Link/@ObjectLink替代@Prop减少深拷贝,可以提升组件创建速度。
因为示例中仅涉及一个简单对象FriendMoment的深拷贝,所以优化时间绝对值较小。如果涉及变量较多、对象较复杂,性能提升会更明显。
3.避免对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新
在父子组件数据同步时,如果子组件已经使用@Link/@ObjectLink/@Prop等会自动同步父子组件数据、且驱动组件刷新的状态变量。不需要再在boutToReuse方法中再进行数据更新,此操作会造成不必要的方法执行和变量更新的耗时。正反例如下:
反例:
@Entry
@Component
struct LessEmbeddedComponent {
@State momentData: FriendMomentsData = new FriendMomentsData();
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({moment: moment})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMoment {
// 该类型的状态变量已包含自动刷新功能,不需要再重复进行刷新
@ObjectLink moment: FriendMoment;
// 此处aboutToReuse为多余刷新
aboutToReuse(params: Record<string, Object>): void {
this.moment.id = (params.moment as FriendMoment).id
this.moment.userName = (params.moment as FriendMoment).userName
this.moment.avatar = (params.moment as FriendMoment).avatar
this.moment.text = (params.moment as FriendMoment).text
this.moment.image = (params.moment as FriendMoment).image
}
build() {
Column() {
...
Text(`${this.moment.userName}`)
...
}
}
}
@Observed
export class FriendMoment {
id: string;
userName: string;
avatar: string;
text: string;
size: number;
image?: string;
constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) {
this.id = id;
this.userName = userName;
this.avatar = avatar;
this.text = text;
this.size = size;
if (image !== undefined) {
this.image = image;
}
}
}
上述反例的操作中,子组件中moment变量被@ObjectLink修饰,把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现数据刷新。重新在aboutToReuse中刷新,如果刷新涉及的变量较多、变量中成员变量复杂,可能会造成较大性能开销。
优化前,由于在复用组件OneMoment的aboutToReuse方法中,对moment变量的各个成员变量进行了刷新,aboutToReuse耗时168μs。
正例:
@Entry
@Component
struct LessEmbeddedComponent {
@State momentData: FriendMomentsData = new FriendMomentsData();
aboutToAppear(): void {
getFriendMomentFromRawfile();
}
build() {
Column() {
TopBar()
List({ space: ListConstants.LIST_SPACE }) {
LazyForEach(momentData, (moment: FriendMoment) => {
ListItem() {
OneMoment({moment: moment})
}
}, (moment: FriendMoment) => moment.id)
}
.cachedCount(Constants.CACHED_COUNT)
}
}
}
@Reusable
@Component
export struct OneMoment {
@ObjectLink moment: FriendMoment;
build() {
Column() {
...
Text(`${this.moment.userName}`)
...
}
}
}
@Observed
export class FriendMoment {
id: string;
userName: string;
avatar: string;
text: string;
size: number;
image?: string;
constructor(id: string, userName: string, avatar: string, text: string, size: number, image?: string) {
this.id = id;
this.userName = userName;
this.avatar = avatar;
this.text = text;
this.size = size;
if (image !== undefined) {
this.image = image;
}
}
}
上述正例的操作中,子组件中moment变量被@ObjectLink修饰,把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现数据刷新。
优化效果
在正反例中,针对列表滑动场景,反例中在aboutToReuse方法中,冗余刷新了自动刷新的变量moment中的各个成员变量。正例中,利用@ObjectLink修饰的变量moment自动同步数据的特性,直接进行刷新,不在aboutToReuse方法再进行刷新。
优化后,避免在复用组件OneMoment的aboutToReuse方法中,重复刷新变量moment的各个成员变量,aboutToReuse耗时110μs。
所以,通过上述Trace数据证明,避免在复用组件中,对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse方法中再进行更新。会减少aboutToReuse方法的时间,进而减少复用组件的创建时间。
因为示例中仅涉及一个简单变量moment的各成员变量的冗余刷新,所以优化时间绝对值不大。如果涉及变量较多、变量中成员变量复杂,性能提升会更明显。
最后
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。
点击领取→【纯血版鸿蒙全套最新学习资料】(安全链接,放心点击)希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!
这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、(南向驱动、嵌入式等)鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。
鸿蒙(HarmonyOS NEXT)最新学习路线
有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。
获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
HarmonyOS Next 最新全套视频教程
《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建
《鸿蒙开发基础》
- ArkTS语言
- 安装DevEco Studio
- 运用你的第一个ArkTS应用
- ArkUI声明式UI开发
- .……
《鸿蒙开发进阶》
- Stage模型入门
- 网络管理
- 数据管理
- 电话服务
- 分布式应用开发
- 通知与窗口管理
- 多媒体技术
- 安全技能
- 任务管理
- WebGL
- 国际化开发
- 应用测试
- DFX面向未来设计
- 鸿蒙系统移植和裁剪定制
- ……
《鸿蒙进阶实战》
- ArkTS实践
- UIAbility应用
- 网络案例
- ……
大厂面试必问面试题
鸿蒙南向开发技术
鸿蒙APP开发必备
鸿蒙生态应用开发白皮书V2.0PDF
总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,才能在这个变革的时代中立于不败之地。