简介
默认的组件复用行为,是将子组件放在父组件的缓存池里,受到这个限制,不同父组件中的相同子组件无法复用,推荐的解决方案是将父组件改为builder函数,让子组件共享组件复用池,但是由于在一些应用场景下,父组件承载了复杂的带状态的业务逻辑,而builder是无状态的,修改会导致难以维护,因此开发者可以使用BuilderNode自行管理组件复用池。
实现思路
- 将要生成自定义组件地方用NodeContainer占位,将NodeContainer内部的NodeController按照组件类型分别存储在NodePool中。
- 每次需要创建子组件时,优先从NodePool中取出一个组件,如果NodePool中没有可复用的组件则重新创建一个,否则就更新一下数据。当NodeController销毁时,回收到NodePool中,供下次使用。
数据结构
创建和销毁过程中复用池的流程
应用场景
在应用开发中,会遇到需要页面切换的场景,比如某些视频APP的首页,就是一个List(标题)+Swiper(列表页面)实现的Tabs切换场景(如图1所示)。Swiper中每个页面都使用瀑布流加载视频列表,各个瀑布流中的子组件有可能是相同的布局,为了提升应用性能,就会有跨页面复用子组件的需求。但是在ArkUI提供的常规复用中,复用池是放在父组件中的,这就导致跨页面时无法复用上一个页面瀑布流中的子组件。此时就可以使用BuilderNode自定义一个全局的组件复用池,根据页面状态创建、回收、复用子组件,实现组件的跨页面复用。
图1 应用场景示意图
示例代码
下面通过常规复用和自定义组件复用池两种方式,对比组件复用的性能。
常规复用
-
使用List+Swiper实现Tabs页面切换。
// ... List() { ForEach(this.arrayTitle, (title: Title, index: number) => { ListItem() { TitleView({ title: title, clickListener: () => { if (title.isSelected) { return; } this.swiperController.changeIndex(index, true); this.arrayTitle[index].isSelected = true; this.arrayTitle[this.selectIndex].isSelected = false; this.selectIndex = index; } }) // ... } }) } .height(30) .listDirection(Axis.Horizontal) Swiper(this.swiperController) { // 使用LazyForEach,使Swiper页面按需加载,而不是一次全部创建 LazyForEach(this.array, () => { TabComp() }, (title: string) => title) } .loop(false) .onChange((index: number) => { if (this.selectIndex !== index) { this.arrayTitle[index].isSelected = true; this.arrayTitle[this.selectIndex].isSelected = false; this.selectIndex = index; } }) .cachedCount(0) // 此处设置cachedCount为0,便于性能对比,实际开发中可按需设置 // ...
-
使用Swiper组件实现轮播图,使用WaterFlow组件实现瀑布流加载数据,并给自定义组件设置reuseId,用于组件复用。
// ... Scroll(this.scroller) { Column({ space: 2 }) { SwiperBuilder({images: this.images}) WaterFlow() { LazyForEach(this.dataSource, (item: ViewItem, index: number) => { FlowItem() { FlowItemComp({ item: item, itemHeight: this.itemHeightArray[index % 100], itemColor: Color.White, updater: (item: ViewItem) => { this.fillNewData(item); } }).reuseId('reuse_type_') } .width('100%') }, (item: string) => item) } // ... .nestedScroll({ // 设置嵌套滑动属性,使 scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) } }.width('100%') .height('100%') // ...
-
实现瀑布流的子组件。
// 需要添加@Reusable装饰器,并实现aboutToReuse接口用于组件复用时刷新数据 @Reusable @Component export struct FlowItemComp { // ... build() { // ... } // 通过aboutToReuse接口刷新复用后的数据 aboutToReuse(params: ESObject): void { this.item = params.item; this.itemHeight = params.itemHeight; this.itemColor = params.itemColor; } }
编译运行后,点击Tabs切换页面,然后抓取Trace,通过图2可以看到,切换Tabs时,每个页面的首帧创建耗时(从点击Tab的DispatchTouchEvent标签开始,到sendCommands标签通知系统绘制组件结束)都在30-40ms左右。这是因为使用@Reusable的组件复用,是使用了父组件的复用池。FlowItemComp的父组件是WaterFlow,Tab切换时新页面的WaterFlow会被重新创建,这就导致前一个页面的复用池是无法使用的,只能重新创建所有的子组件。
图2 常规复用Trace图
自定义组件复用池
-
使用List+Swiper实现Tabs页面切换。
// ... Swiper(this.swiperController) { LazyForEach(this.array, () => { TabNode() }, (title: string) => title) } // ...
-
继承NodeController,实现makeNode,用于组件的创建或刷新,并在组件隐藏时(aboutToDisappear)回收组件。
export class NodeItem extends NodeController { private callback: UpdaterCallback | null = null; // ... // 父类方法,用于创建子组件 makeNode(uiContext: UIContext): FrameNode | null { if (!this.node) { this.node = new BuilderNode(uiContext); this.node.build(this.builder, this.data); } else { this.node.update(this.data); this.update(this.data); } return this.node.getFrameNode(); } // 组件隐藏时回收组件 aboutToDisappear(): void { NodePool.getInstance().recycleNode(this.type, this); } // ... }
-
使用单例模式实现复用池,应用内统一管理组件复用
export class NodePool { private static instance: NodePool; // ... private constructor() { this.nodePool = new HashMap(); this.nodeHook = new HashSet(); this.idGen = 0; } // 单例模式,可以全局统一管理 public static getInstance() { if (!NodePool.instance) { NodePool.instance = new NodePool(); } return NodePool.instance; } // ... }
-
添加getNode方法,根据传入的type参数,获取对应的Node组件,如果未找到,则重新创建
// ... // 获取Node组件,如果存在type类型的Node组件,则直接使用,否则重新创建 public getNode(type: string, data: ESObject, builder: WrappedBuilder<ESObject>): NodeItem | undefined { let node: NodeItem | undefined = this.nodePool.get(type)?.pop(); if (!node) { node = new NodeItem(builder, data, type); this.nodeHook.add(node); } else { node.data = data; } node.data.callback = (callback: UpdaterCallback) => { if (node) { node.registerUpdater(callback); } } return node; } // ...
-
实现recycleNode方法,回收Node组件
// 回收Node组件,提供给下次复用 public recycleNode(type: string, node: NodeItem) { let nodeArray: Array<NodeItem> = this.nodePool.get(type); if (!nodeArray) { nodeArray = new Array(); this.nodePool.set(type, nodeArray); } nodeArray.push(node); }
-
使用NodeContainer占位轮播图组件和瀑布流子组件的位置,在最外层的Swiper切换时,会根据LazyForEach的懒加载机制回收页面,此时会触发NodeItem中的aboutToDisappear方法,将组件回收到复用池中。而新加载的页面则可以通过自定义的组件复用池获取可用的子组件,如果未获取到对应type类型的组件,则会重新创建新的组件,否则直接获取之前回收的子组件进行复用。
// ... @Builder function FlowItemBuilder(data: ESObject) { FlowItemNode({ item: data.item, itemHeight: data.itemHeight, itemColor: data.itemColor, updater: data.updater, callback: data.callback }) } let flowItemWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(FlowItemBuilder); let swiperWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(SwiperBuilder); @Component export struct TabNode { // ... build() { Scroll(this.scroller) { Column({ space: 2 }) { NodeContainer(NodePool.getInstance().getNode('reuse_type_swiper_', { images: this.images }, swiperWrapper)) WaterFlow() { LazyForEach(this.dataSource, (item: ViewItem, index: number) => { FlowItem() { NodeContainer(NodePool.getInstance().getNode('reuse_type_', { item: item, itemHeight: this.itemHeightArray[index % 100], itemColor: this.colors[index % 5], updater: (item: ViewItem) => { this.fillNewData(item); }, callback: null }, flowItemWrapper)) } .width('100%') }, (item: string) => item) } // ... } }
编译运行后,点击Tabs切换页面,然后抓取Trace,通过图3可以看到,风景页面的首帧创建耗时和常规复用是差不多的,但是后面2个页面的耗时大幅减少,只有14ms和17ms左右。这是因为风景页面创建时自定义复用池里没有可复用的子组件,所以会和常规复用一样,直接创建新的子组件。而切换到商品页面时,首页里面的子组件被回收到了自定义复用池NodePool中,当商品页面被创建时,会先去复用池中查找可用的子组件直接使用,减少了创建子组件的时间。
图3 自定义组件复用池Trace图
性能数据对比
页面 | 风景 | 商品 | 旅游 | 头像 |
---|---|---|---|---|
创建耗时(优化前) | 39.5ms | 35.7ms | 29.8ms | 26.5ms |
创建耗时(优化后) | 40.3ms | 14.8ms | 17.8ms | 18.3ms |
总结
在父组件内部进行组件复用时,使用常规复用是可以解决问题的,而且使用简单,只需要添加@Reusable装饰器并且实现aboutToReuse。但是由于复用池的局限性,不同的父组件想要复用相同子组件时就会失效。而自定义组件复用池,可以实现跨页面的组件复用,但是实现起来也比较复杂,需要开发者自己维护复用池。
FAQ
**Q:**示例代码中为什么不使用ArkUI提供的Tabs+TabContent组件,而是要用List+Swiper组件实现?
**A:**Tabs中不支持使用LazyForEach,只能使用ForEach。如果使用ForEach,那么在页面创建时会将所有的TabContent全部创建,并且切换时无法回收子组件(不会执行aboutToDisappear),这就导致自定义复用池NodePool中是空的,每次创建时都获取不到组件,只能重新创建,使组件复用失去了效果。并且因为多创建了一个NodeContainer组件,耗时会比常规复用更长。
最后
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(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
总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,才能在这个变革的时代中立于不败之地。