本节目标
- 理解 ArkUI 三类自定义节点(FrameNode、RenderNode、BuilderNode)的定位与协作关系
- 掌握
FrameNode的创建、节点树操作(增删查改)与挂载显示方法 - 掌握
NodeController与NodeContainer的配合机制,理解自定义节点的挂载流程 - 掌握
RenderNode的自定义渲染能力,能够通过getRenderNode()获取渲染节点并进行视觉属性设置与自定义绘制 - 掌握
BuilderNode的组件预创建能力,能够利用预创建减少首屏加载耗时 - 理解全局自定义组件复用池的设计思路,能够利用
BuilderNode+NodePool实现跨页面组件复用 - 能够根据场景选择正确的自定义节点方案,并遵循节点唯一父节点等约束
一、自定义节点体系概述
1.1 三类自定义节点的定位
ArkUI 提供三种自定义节点对象,各自解决不同层面的问题:
FrameNode:表示组件树的实体节点,是自定义组件节点。支持节点树操作、自定义绘制与布局、位置查询、动画等能力。适用于需要通过代码动态创建和管理组件节点树的场景,可实现声明式组件无法直接满足的灵活 UI 组合与自定义渲染需求。
RenderNode:自绘制渲染节点,支持开发者完成自定义绘制需求。还支持渲染节点树管理(添加、删除、查询子节点)、背景色与不透明度等视觉属性设置、变换(缩放、旋转、平移、变换矩阵)、阴影、边框、遮罩与裁剪、模糊效果等能力,适用于在 Stage 模型下进行自定义渲染与节点树管理的场景。
BuilderNode:赋予创建、更新系统组件及其组件树的能力,使用@Builder装饰的无状态 UI 方法生成组件树。其核心价值在于组件预创建,能够在非 build 生命周期中提前创建组件,节省组件创建时间。
1.2 三者的协作关系
NodeController用于管理自定义节点的创建、显示与更新,负责将节点挂载到NodeContainer上。NodeContainer 作为自定义占位节点,在组件树上为自定义节点提供挂载点。
典型协作流程为:创建自定义节点(FrameNode 或 BuilderNode 持有的 FrameNode)→ 实现 NodeController 的makeNode()方法返回节点 → 通过 NodeContainer 将节点挂载到组件树上显示。
1.3 核心约束
使用自定义节点时必须遵守以下约束:
- 节点唯一父节点:同一时刻同一个节点有且仅有一个父节点。作为不同节点的子节点存在时,可能出现事件响应异常、内容显示异常、位置显示异常、稳定性问题。
- 避免网状或环形结构:父子节点相互引用会导致节点树成环形,一个子节点有多个父节点会导致节点树成网状。
- 不在预览器中使用:FrameNode 节点当前不支持在预览器中使用,FrameNode 节点暂不支持拖拽。
二、FrameNode:组件实体节点
2.1 FrameNode 的两类能力
FrameNode 提供两类核心能力:
完全自定义节点:提供完整的自定义能力,包括自定义测量、布局以及绘制,支持节点的动态增、删,设置通用属性,设置事件回调。通过new FrameNode(uiContext)或typeNode.createNode('Text')等方法创建出来的节点属于完全自定义节点。适用于不自带渲染引擎、需要依赖系统的布局、事件、动画、渲染等能力的场景。
系统组件代理节点:提供系统组件的代理能力,提供遍历节点树的能力,通过组件树上的 FrameNode 可以遍历整个组件树,并通过节点访问组件的信息或者注册额外的事件监听回调。适用于结合无感监听的接口实现打点、广告 SDK 等业务。
2.2 节点树操作
FrameNode 提供了增、删、查、改的节点树操作能力。核心方法包括appendChild(在最后一个子节点后添加新子节点)、removeChild(移除子节点)、getChild(获取指定索引的子节点)、getChildrenCount(获取子节点数量)等。
import{FrameNode,NodeController,UIContext}from'@kit.ArkUI';classMyNodeControllerextendsNodeController{privaterootNode:FrameNode|null=null;makeNode(uiContext:UIContext):FrameNode|null{this.rootNode=newFrameNode(uiContext);// 创建子节点并添加到根节点consttextNode=typeNode.createNode(uiContext,'Text');textNode.initialize('动态创建的文本节点');textNode.attribute.fontSize(20);textNode.attribute.fontColor('#007DFF');this.rootNode.appendChild(textNode);constbuttonNode=typeNode.createNode(uiContext,'Button');buttonNode.initialize('动态按钮');this.rootNode.appendChild(buttonNode);returnthis.rootNode;}// 动态添加节点的方法addNewNode(uiContext:UIContext):void{if(!this.rootNode)return;constnewNode=typeNode.createNode(uiContext,'Text');newNode.initialize(`新节点${this.rootNode.getChildrenCount()}`);newNode.attribute.fontSize(16);this.rootNode.appendChild(newNode);}// 移除最后一个子节点removeLastNode():void{if(!this.rootNode||this.rootNode.getChildrenCount()===0)return;constlastIndex=this.rootNode.getChildrenCount()-1;constchild=this.rootNode.getChild(lastIndex);this.rootNode.removeChild(child);}}@Entry@Componentstruct FrameNodeDemo{privatecontroller:MyNodeController=newMyNodeController();build(){Column({space:16}){NodeContainer(this.controller).width('100%').height(200).backgroundColor('#F5F7FA').borderRadius(12)Row({space:12}){Button('添加节点').onClick(()=>{constuiContext=this.getUIContext();this.controller.addNewNode(uiContext);})Button('移除节点').onClick(()=>this.controller.removeLastNode())}}.width('100%').padding(16)}}2.3 FrameNode 的性能优势
在动态布局场景下,FrameNode 相比声明式自定义组件具有显著优势:
减少创建开销:使用 FrameNode 可以避免创建自定义组件对象和状态变量对象,无需进行依赖收集,从而显著提升组件创建的速度。声明式开发中每个节点都需要在 ArkTS 引擎中分配内存空间来存储自定义组件和状态变量,并执行组件 ID、组件闭包以及状态变量之间的依赖关系收集等操作。
更新更快:对复杂组件树(深度超过 30 层,包含 100 至 200 个组件)执行 diff 算法时,几乎无法在 120Hz 刷新率下保持满帧。使用 FrameNode 扩展,框架能够自主掌控更新流程,实现高效的按需剪枝。
直接操作组件树:将组件树中的一个子树从当前子节点完整移到另一个子节点时,声明式开发范式只能通过重新渲染整棵组件树的方式实现。使用 FrameNode 扩展可以方便地操控该子树,将其移植到另一个节点,只进行局部渲染刷新。
三、RenderNode:自定义渲染节点
3.1 获取 RenderNode
通过 FrameNode 的getRenderNode()方法可以获取 FrameNode 中持有的 RenderNode。RenderNode 提供了appendChild方法在最后一个子节点后添加新的子节点。
import{RenderNode,FrameNode,NodeController,UIContext}from'@kit.ArkUI';classRenderNodeControllerextendsNodeController{privaterootNode:FrameNode|null=null;makeNode(uiContext:UIContext):FrameNode|null{this.rootNode=newFrameNode(uiContext);constrootRenderNode=this.rootNode.getRenderNode();if(rootRenderNode!==null){// 创建一个 RenderNode 并设置属性constrenderNode=newRenderNode();renderNode.frame={x:0,y:0,width:100,height:100};renderNode.backgroundColor=0xFFFF5500;renderNode.borderRadius=12;rootRenderNode.appendChild(renderNode);}returnthis.rootNode;}}3.2 视觉属性设置
RenderNode 支持丰富的视觉属性设置,包括背景色、不透明度、边框、阴影、圆角、变换矩阵(缩放、旋转、平移)、遮罩与裁剪、模糊效果等。开发者可以通过 RenderNode 的属性动态修改节点的渲染效果。
3.3 自定义绘制
RenderNode 允许通过重写draw方法自定义绘制内容。绘制时使用DrawContext提供的绘图能力完成自定义渲染:
import{RenderNode,DrawContext,FrameNode,NodeController,UIContext}from'@kit.ArkUI';classCustomDrawControllerextendsNodeController{privaterootNode:FrameNode|null=null;makeNode(uiContext:UIContext):FrameNode|null{this.rootNode=newFrameNode(uiContext);constrootRenderNode=this.rootNode.getRenderNode();if(rootRenderNode!==null){constrenderNode=newRenderNode();renderNode.frame={x:0,y:0,width:200,height:200};renderNode.draw((context:DrawContext)=>{// 使用 DrawContext 进行自定义绘制constcanvas=context.canvas;// 绘制内容...});rootRenderNode.appendChild(renderNode);}returnthis.rootNode;}}3.4 使用约束
不建议对 BuilderNode 中的 RenderNode 进行修改操作。BuilderNode 中持有的 FrameNode 仅用于将该 BuilderNode 作为子节点挂载到其他 FrameNode 上,对该 FrameNode 或对应的 RenderNode 进行属性设置与子节点操作可能会产生未定义行为,包括但不限于显示异常、事件异常、稳定性问题。
四、BuilderNode 与组件预创建
4.1 预创建原理
在声明式范式中,组件仅在 build 环节中被创建,开发者无法在其他生命周期阶段进行组件创建,从而引起页面加载慢等问题。ArkUI 提供的 UI 动态操作支持组件的预创建,可以在非 build 生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作,之后在页面加载时进行使用,从而极大提升页面响应速度。
利用组件预创建机制,可以在动画执行过程的空闲时间进行组件预创建和属性设置,动画结束后再进行属性和布局的更新,节省组件创建时间,加快页面渲染。
4.2 预创建的典型应用
BuilderNode 预创建对于那些创建初始化耗时较长的声明式组件特别有效,比如 Web、XComponent 等,预创建可以有效减少组件初始化的耗时。
小红书 APP 在冷启动优化中使用了这一方案:在启动阶段先获取磁盘上的笔记内容,然后在 BuilderNode 中预先创建多个 Image 组件。这样在等待网络或推荐接口响应时,Image 组件已经创建完毕,首页刷新时可以立即使用这些组件,从而提升首刷速度。
import{BuilderNode,FrameNode,NodeController,UIContext}from'@kit.ArkUI';classPreCreateControllerextendsNodeController{privaterootNode:FrameNode|null=null;makeNode(uiContext:UIContext):FrameNode|null{this.rootNode=newFrameNode(uiContext);constrootRenderNode=this.rootNode.getRenderNode();if(rootRenderNode!==null){// 预创建 BuilderNodeconstbuilderNode=newBuilderNode(uiContext,{selfIdealSize:{width:200,height:100}});// 使用 WrappedBuilder 构建组件树builderNode.build(newWrappedBuilder(this.buildContent));// 将预创建的节点挂载到渲染节点constrenderNode=builderNode.getFrameNode()?.getRenderNode();if(renderNode!==null){rootRenderNode.appendChild(renderNode);}}returnthis.rootNode;}@BuilderbuildContent(){Column({space:8}){Text('预创建的内容').fontSize(18)Button('操作按钮').fontSize(14)}.padding(12)}}五、全局自定义组件复用池
5.1 为什么需要全局复用池
在默认的组件复用模式下,子组件被放置在父组件的缓存池中,不同父组件中的相同子组件无法复用。将父组件改为 Builder 函数可以让子组件共享组件复用池,但当父组件承载复杂业务逻辑和状态、而 Builder 是无状态的时候,这种方案不可行。
通过 BuilderNode 可以管理组件复用池,实现跨页面的组件复用。
5.2 实现路线
使用NodeContainer作为自定义组件生成位置的占位符,将NodeController存储在NodePool中,按组件类型分类:
复用获取:每次创建子组件时优先使用NodePool的getNode方法复用已有的NodeController组件。如果复现成功,调用update方法更新组件数据。如果没有可复用的组件,则调用makeNode方法创建新的子组件。
回收归还:销毁NodeController组件时,将NodeItem回收到NodePool中供后续使用。页面退出时系统默认会销毁所有节点及其 NodeItem 实例,应用侧可以主动保存 NodeItem 实例,有效延长其生命周期,避免不必要的重建开销。
5.3 数据结构
NodeItem继承NodeController并实现makeNode方法。NodePool使用 HashMap 管理NodeItem的复用与回收,核心方法包括getNode(获取可复用节点)和recycleNode(回收节点)。
// NodePool 核心结构示意classNodeItemextendsNodeController{privatebuilder:WrappedBuilder<[ESObject]>;privatenode:BuilderNode<[ESObject]>|null=null;privatedata:ESObject;privatetype:string;makeNode(uiContext:UIContext):FrameNode|null{this.node=newBuilderNode(uiContext);this.node.build(this.builder);returnthis.node.getFrameNode();}update(data:ESObject):void{this.data=data;// 更新组件数据}aboutToDisappear():void{// 回收时清理}}classNodePool{privatestaticinstance:NodePool|null=null;privatenodePool:Map<string,NodeItem[]>=newMap();staticgetInstance():NodePool{if(!NodePool.instance){NodePool.instance=newNodePool();}returnNodePool.instance;}getNode(type:string,data:ESObject,builder:WrappedBuilder<[ESObject]>):NodeItem{constpool=this.nodePool.get(type);if(pool&&pool.length>0){constitem=pool.pop()!;item.update(data);returnitem;}returnnewNodeItem(builder,data,type);}recycleNode(type:string,node:NodeItem):void{if(!this.nodePool.has(type)){this.nodePool.set(type,[]);}this.nodePool.get(type)!.push(node);}}5.4 典型应用场景
页面切换场景中,首页的 Tabs 页面通过 List(标题)和 Swiper(列表页)实现。Swiper 中每个页面使用瀑布流加载视频列表,各个瀑布流中的子组件可能具有相同布局。这些子组件期望跨页面复用,但 ArkUI 提供的普通复用中复用池位于父组件内,导致上一个页面瀑布流中的子组件无法跨页面复用。此时可以使用 BuilderNode 自定义全局组件复用池,根据页面状态创建、回收和复用子组件。
六、多元化习题
习题 1(判断题)
题目:在 ArkUI 中,同一个自定义节点可以同时作为多个父节点的子节点,不会产生异常。
答案:错误
解读:使用自定义节点相关能力时,需要保证同一时刻同一个节点有且仅有一个父节点。同一个节点作为不同节点的子节点存在时,可能出现事件响应异常、内容显示异常、位置显示异常、稳定性问题等。
习题 2(单选题)
题目:以下哪个自定义节点用于表示组件树的实体节点,支持节点树操作与自定义布局?
A. RenderNode
B. BuilderNode
C. FrameNode
D. NodeContainer
答案:C
解读:FrameNode 表示组件树的实体节点,支持节点树操作、自定义绘制与布局、位置查询、动画等能力。RenderNode 是自绘制渲染节点,BuilderNode 用于创建系统组件树,NodeContainer 是挂载自定义节点的占位容器。
习题 3(多选题)
题目:关于 FrameNode 相比声明式自定义组件的性能优势,以下说法正确的有(多选):
A. FrameNode 可以避免创建自定义组件对象和状态变量对象,无需进行依赖收集
B. FrameNode 可以避免对复杂组件树的大量 diff 操作
C. FrameNode 可以直接操作组件树,将子树从当前子节点移植到另一个节点
D. FrameNode 在预览器中也可以正常使用
答案:A、B、C
解读:使用 FrameNode 可以避免创建自定义组件对象和状态变量对象,无需进行依赖收集,选项 A 正确。使用 FrameNode 扩展框架能够自主掌控更新流程,实现高效的按需剪枝,避免大量 diff 操作,选项 B 正确。使用 FrameNode 可以通过操作节点方便地操控子树,将其移植到另一个节点,只进行局部渲染刷新,选项 C 正确。当前不支持在预览器中使用 FrameNode 节点,选项 D 错误。
习题 4(代码填空题)
题目:请补全以下代码,获取 FrameNode 中持有的 RenderNode,并将一个自定义 RenderNode 挂载到其上。
classMyControllerextendsNodeController{privaterootNode:FrameNode|null=null;makeNode(uiContext:UIContext):FrameNode|null{this.rootNode=newFrameNode(uiContext);// 在此处填写代码,获取 RenderNodeconstrootRenderNode=this.rootNode.______________();if(rootRenderNode!==null){constrenderNode=newRenderNode();renderNode.frame={x:0,y:0,width:100,height:100};renderNode.backgroundColor=0xFFFF5500;rootRenderNode.______________(renderNode);}returnthis.rootNode;}}答案:getRenderNode、appendChild
解读:通过getRenderNode()方法可以获取 FrameNode 中持有的 RenderNode。通过appendChild()方法在 RenderNode 的最后一个子节点后添加新的子节点。
习题 5(代码改错题)
题目:以下代码存在节点树结构错误,请指出问题并修正。
constnodeA=typeNode.createNode(uiContext,'Text');nodeA.initialize('A');constnodeB=typeNode.createNode(uiContext,'Text');nodeB.initialize('B');constnodeC=typeNode.createNode(uiContext,'Text');nodeC.initialize('C');// 节点树挂载nodeA.appendChild(nodeB);nodeA.appendChild(nodeC);nodeB.appendChild(nodeC);// 问题:nodeC 同时作为 nodeA 和 nodeB 的子节点答案:nodeC同时作为nodeA和nodeB的子节点,违反了“节点唯一父节点”的约束,会导致事件响应异常、内容显示异常等问题。修正方案是移除重复的挂载:
nodeA.appendChild(nodeB);nodeA.appendChild(nodeC);// 删除 nodeB.appendChild(nodeC);解读:使用自定义节点相关能力时,需要保证同一时刻同一个节点有且仅有一个父节点。同一个节点作为不同节点的子节点存在时,可能出现事件响应异常、内容显示异常、位置显示异常、稳定性问题。
习题 6(简答题)
题目:简述 FrameNode、RenderNode、BuilderNode 三类自定义节点的区别与协作关系。
答案:FrameNode 是组件树的实体节点,支持节点树操作、自定义绘制与布局,适用于动态创建和管理组件节点树。RenderNode 是更轻量级的渲染节点,专注于自定义渲染,支持背景色、变换、阴影、模糊等视觉属性设置。BuilderNode 赋予创建、更新系统组件及其组件树的能力,使用@Builder装饰的无状态 UI 方法生成组件树,核心价值在于组件预创建和全局组件复用池管理。协作关系为:NodeController 管理节点的创建与更新,通过 makeNode 返回 FrameNode 或 BuilderNode 持有的 FrameNode,再通过 NodeContainer 将节点挂载到组件树上显示。FrameNode 可以通过 getRenderNode 获取对应的 RenderNode 进行渲染层面的操作。
解读:三类节点分别对应“组件树操作”、“渲染控制”和“组件创建复用”三个层面。FrameNode 是最基础的实体节点,RenderNode 在其基础上提供渲染能力,BuilderNode 则封装了组件树的创建过程。
习题 7(简答题)
题目:简述如何利用 BuilderNode 和 NodePool 实现全局自定义组件复用池,以及其解决的核心问题。
答案:实现方式为:使用 NodeContainer 作为占位符,将 NodeController 存储在 NodePool 中按类型分类。每次创建子组件时优先使用 NodePool 的 getNode 方法复用已有组件,复用成功则调用 update 更新数据;无可用组件时调用 makeNode 创建。销毁时调用 recycleNode 将 NodeItem 归还到 NodePool。NodeItem 继承 NodeController 并实现 makeNode 方法,NodePool 使用 HashMap 管理 NodeItem 的复用与回收。核心解决的问题是:在默认的组件复用模式下,子组件被放置在父组件的缓存池中,不同父组件中的相同子组件无法复用。通过 BuilderNode 管理全局复用池,可以实现跨页面的组件复用,避免页面退出时节点被全部销毁导致的重复创建开销。
解读:全局复用池的关键在于将复用池的管理权从父组件提升到全局层面,通过 NodePool 统一管理 NodeItem 的创建、更新、回收和复用,从而突破父组件缓存池的局限。
七、本节知识点总结
自定义节点体系
ArkUI 提供三类自定义节点:FrameNode(组件实体节点)、RenderNode(自绘制渲染节点)、BuilderNode(组件树创建与预创建节点)。通过 NodeController 和 NodeContainer 完成节点的创建与挂载。
FrameNode 节点树操作
支持 appendChild、removeChild、getChild、getChildrenCount 等增删查改能力。完全自定义节点支持自定义测量、布局和绘制,系统组件代理节点支持遍历组件树和访问组件信息。
FrameNode 性能优势
避免创建自定义组件对象和状态变量对象,无需依赖收集;框架自主掌控更新流程,实现按需剪枝;可直接操作组件树移植子树,只进行局部渲染刷新。
RenderNode 自定义渲染
通过 FrameNode 的 getRenderNode() 获取。支持丰富的视觉属性设置(背景色、变换、阴影、模糊等),支持自定义绘制。不建议对 BuilderNode 中的 RenderNode 进行修改操作。
BuilderNode 组件预创建
在非 build 生命周期中提前创建组件,适用于 Web、XComponent 等初始化耗时较长的组件。小红书 APP 利用预创建 Image 组件优化冷启动首刷速度。
全局组件复用池
通过 BuilderNode + NodePool 实现跨页面组件复用,突破父组件缓存池的局限。NodeItem 继承 NodeController,NodePool 使用 HashMap 管理复用与回收。
核心约束
节点唯一父节点,避免网状或环形结构,不在预览器中使用,FrameNode 节点暂不支持拖拽。
下节预告
第4课将进入 ArkUI 的 NDK 开发与 C++ 混合编程的学习,涵盖 Node-API 的 C++ 接口封装、Native 侧组件节点的创建与操作、以及 ArkTS 与 C++ 的高效数据交互方案。