简介
为了解决页面、组件加载缓慢的问题,ArkUI框架提供了动态操作以实现组件预创建,并允许应用在运行时根据实际需要加载渲染相应的组件。动态操作包含动态创建组件(动态添加组件)、动态卸载组件(动态删除组件)等相关操作。动态创建组件指在非build生命周期中进行组件创建,即在build生命周期前提前创建组件。通过动态创建组件,不但可以节省组件创建的时间,提升用户体验,还可以将独立的逻辑进行封装,有助于应用模块化开发。动态卸载组件是对动态创建的组件进行卸载、删除。
组件预创建原理
在声明式范式中,组件仅在build环节中被创建,开发者无法在其他生命周期阶段进行组件的创建,从而引起页面加载慢等问题。与声明式范式不同,ArkUI框架提供的UI动态操作支持组件的预创建。组件预创建可以满足开发者在非build生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作。之后在页面加载时进行使用,可以极大提升页面响应速度。
如下图所示,利用组件预创建机制,可以利用动画执行过程空闲时间进行组件预创建和属性设置。在动画结束后,再进行属性和布局的更新,节省了组件创建的时间,从而加快了页面渲染。
图1 组件预创建原理图
FrameNode自定义节点在动态布局场景下的优势
减少自定义组件创建开销
在采用声明式开发范式中,若使用ArkUI的自定义组件对节点树中的每个节点进行定义,往往会遇到节点创建效率低下的问题。
这主要是因为每个节点在ArkTS引擎中都需要分配内存空间来存储应用程序的自定义组件和状态变量。在节点创建过程中,还必须执行组件ID、组件闭包以及状态变量之间的依赖关系收集等操作。
相比之下,使用ArkUI的[FrameNode] ,可以避免创建自定义组件对象和状态变量对象,无需进行依赖收集,从而显著提升组件创建的速度。
组件更新更快
在动态布局类框架的更新场景中,通常存在一个由树形数据结构ViewModelA创建的UI组件树TreeA。当需要使用新的数据结构ViewModelB来更新TreeA时,尽管声明式开发范式可以实现数据驱动的自动更新,但这一过程中却伴随着大量的diff操作,如下图所示。对于ArkTS引擎而言,在对一个复杂组件树(深度超过30层,包含100至200个组件)执行diff算法时,几乎无法在120Hz的刷新率下保持满帧运行。然而,使用ArkUI的FrameNode扩展,框架能够自主掌控更新流程,实现高效的按需剪枝。特别是针对那些仅服务于少数特定业务的动态布局框架,利用这一扩展,可以实现快速的更新操作。
直接操作组件树
使用声明式开发范式还存在组件树结构更新操作困难的痛点,比如将组件树中的一个子树从当前子节点完整移到另一个子节点,使用声明式开发范式无法直接调整组件实例的结构关系,只能通过重新渲染整棵组件树的方式实现上述操作。而使用ArkUI的FrameNode扩展,则可以通过操作FrameNode来很方便的操控该子树,将其移植到另一个节点,这样只会进行局部渲染刷新,性能更优。
组件动态添加、更新和删除:
动态添加组件
动态添加组件包括以下步骤:
- 创建自定义节点。
- 实现[NodeController] ,用于自定义节点的创建、显示、更新等操作的管理,并负责将自定义节点挂载到[NodeContainer] 上。
- 实现NodeController的makeNode方法,makeNode会在NodeController实例绑定NodeContainer的时候进行回调,并将返回的节点挂载至NodeContainer。
- 使用NodeContainer显示自定义节点。
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
class Params {
text: string = 'Hello World';
constructor(text: string) {
this.text = text;
}
}
@Builder
function buildText(params: Params) {
Column() {
Text(params.text)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.margin({bottom: 36})
}
}
......
// ...
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private message: string = '';
constructor(message: string) {
super();
this.message = message;
}
makeNode(context: UIContext): FrameNode | null {
return null;
}
}
-
实现NodeController的makeNode方法
首先,使用构造函数创建BuilderNode实例。创建BuilderNode对象的时候必须要传入对应的UIContext对象。若BuilderNode作为RenderNode的子节点存在,要求设置RenderOptions的selfIdealSize属性。
然后,使用BuilderNode的build方法,构建组件树。方法build()需要传入两个参数,第一个参数为通过wrapBuilder()封装的全局@Builder方法。第二个参数为对应的@Builder方法所需的参数对象。若@Builder方法不带参数或者存在默认参数,则build()的第二个参数可以不设置。
// ...
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private message: string = '';
constructor(message: string) {
super();
this.message = message;
}
makeNode(context: UIContext): FrameNode | null {
// 创建BuilderNode实例
this.textNode = new BuilderNode(context);
// 设置selfIdealSize属性
// this.textNode = new BuilderNode(context, {selfIdealSize: {width: 100, height :100}});
// 使用build方法构建组件树
this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
// 返回需要显示的节点
return this.textNode.getFrameNode();
}
}
......
-
显示自定义节点
显示自定义节点依赖声明式渲染容器NodeContainer以及对应的控制类NodeController。
NodeController的makeNode方法返回的节点会显示在对应的NodeContainer中。由于makeNode需要返回的为一个FrameNode,因此如果预期显示BuidlerNode的时候需要调用对应的BuidlerNode的getFrameNode的方法,获取其根节点,详细代码如上TextNodeController中所示。
然后,在页面内新增声明式渲染容器NodeContainer,创建工具类NodeController。通过NodeController将MakeNode中返回的节点在声明式渲染容器中进行显示。
// ...
@Entry
@Component
struct Index {
@State message: string = "hello";
private textNodeController: TextNodeController = new TextNodeController(this.message);
build() {
Row() {
Column() {
NodeContainer(this.textNodeController)
.width('100%')
.height(100)
.backgroundColor('#FFF0F0F0')
}
.width('100%')
.height('100%')
}
.height('100%')
}
}
动态删除组件
通过条件控制语句可以将NodeContainer节点进行移除或者显示。如示例代码,将this.isShow更改为false则将节点从界面上移除。
// ...
@Entry
@Component
struct Index {
@State message: string = "hello";
@State isShow: boolean = true;
private textNodeController: TextNodeController = new TextNodeController(this.message);
build() {
Row() {
Column() {
if (this.isShow) {
NodeContainer(this.textNodeController)
.width('100%')
.height(100)
.backgroundColor('#FFF0F0F0')
}
Button('isShow')
.onClick(() => {
this.isShow = false;
})
}
.width('100%')
.height('100%')
}
.height('100%')
}
}
动态更新组件
动态将NodeContainer上的节点替换,依赖于NodeController的makeNode和rebuild方法。rebuild方法会触发makeNode的回调,刷新NodeContainer上显示的节点;makeNode方法返回的为null,则移除NodeContainer下挂载的节点。
// ...
class TextNodeController extends NodeController {
private textNode: BuilderNode<[Params]> | null = null;
private message: string = '';
constructor(message: string) {
super();
this.message = message;
}
makeNode(context: UIContext): FrameNode | null {
// 加上判空处理,只有第一次创建BuilderNode时,才会执行下列代码;替换节点时,textNode不为null
if (this.textNode == null) {
this.textNode = new BuilderNode(context);
this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
}
return this.textNode.getFrameNode();
}
replaceBuilderNode(newNode: BuilderNode<Object[]>) {
this.textNode = newNode;
// rebuild方法会重新调用makeNode方法
this.rebuild();
}
}
......
开发者可以根据实际情况创建新的节点,参考示例代码如下所示:
// ...
@Entry
@Component
struct Index {
@State message: string = "hello";
@State isShow: boolean = true;
private textNodeController: TextNodeController = new TextNodeController(this.message);
// private count = 0;
build() {
Row() {
Column() {
if (this.isShow) {
NodeContainer(this.textNodeController)
.width('100%')
.height(100)
.backgroundColor('#FFF0F0F0')
}
Button('replaceNode')
.onClick(() => {
this.textNodeController.replaceBuilderNode(this.buildNewNode());
})
}
.width('100%')
.height('100%')
}
.height('100%')
}
buildNewNode(): BuilderNode<[Params]> {
let uiContext: UIContext = this.getUIContext();
let message = 'newNode';
let textNode = new BuilderNode<[Params]>(uiContext);
textNode.build(wrapBuilder<[Params]>(buildText), new Params(message))
return textNode;
}
}
NodeController生命周期
NodeController用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。下面,对其常用生命周期函数进行说明。
- makeNode必须要重写的方法,用于构建节点树、返回节点挂载在对应NodeContainer中。在对应NodeContainer创建绑定当前NodeController的时候调用、或者通过rebuild方法调用刷新。
- aboutToResize当controller对应的NodeContainer在Mesure的时候进行回调,入参为节点的布局大小。
- aboutToAppear当controller对应的NodeContainer在onAppear的时候进行回调。
- aboutToDisappear当controller对应的NodeContainer在onDisappear的时候进行回调。
export abstract class NodeController {
abstract makeNode(uiContext: UIContext): FrameNode | null;
aboutToResize?(size: Size): void;
aboutToAppear?(): void;
aboutToDisappear?(): void;
rebuild(): void;
onTouchEvent?(event: TouchEvent): void;
}
三方广告SDK实践案例
场景描述
应用的广告业务一般都由三方广告商提供,可以使用动态加载组件的方式,提前预加载三方广告组件,提升页面加载的速度。以应用开屏广告为例,在应用启动时且在应用主界面显示之前,用户可以点击跳过广告直接进入应用主界面。
实现方案介绍
对于三方广告SDK场景,如下图所示。广告SDK负责维护广告组件,使用自定义节点BuilderNode挂载原生图文等组件节点来提供广告内容与广告页面交互逻辑;接入广告SDK的应用通过NodeContainer预留布局空间,用于展示广告SDK提供的广告组件。NodeController用于绑定自定义广告组件节点与NodeContainer布局组件。
图2 三方广告SDK场景图
案例参考
以一个案例来展示三方广告SDK如何提供广告组件,以及应用如何集成广告SDK提供的广告组件。案例效果如下:
图3 实现效果图
整体实现步骤如下所示:
- 三方广告侧创建广告的自定义构建函数。
- 三方广告侧创建自定义广告节点控制器,实现NodeController的makeNode方法,为应用侧提供NodeController。
- 应用侧引用NodeController,并与NodeContainer进行绑定,进而展示广告。
首先,广告SDK创建@Builder自定义构建函数。广告SDK提供各种类型的广告,需要在广告SDK中创建广告组件的@Builder自定义构建函数。以开屏广告为例来讲解,代码如下。其中ADNodeBuilder()为自定义构建函数,用于构建广告组件节点,该广告节点示例代码中包含一个广告图片、一段文本和一个可以跳转广告详情的按钮。ADNodeParams参数类用于为ADNodeBuilder() 自定义构建函数提供参数化数据,以展示不同的广告信息。
import { NodeController, BuilderNode, FrameNode } from '@kit.ArkUI';
// 广告SDK对外提供的广告组件节点的参数
class ADNodeParams {
adImg: string = '';
adText: string = '';
adLink: string = '';
constructor(adImg: string, adText: string, adLink: string) {
this.adImg = adImg;
this.adText = adText;
this.adLink = adLink;
}
}
// 广告SDK对外提供的广告组件节点的自定义构建函数
@Builder
function ADNodeBuilder(params: ADNodeParams) {
//该广告节点示例,包含一个广告图片
Stack() {
Image($r(params.adImg))
.objectFit(ImageFit.Contain)
.height('100%')
.width('100%')
}.height('100%')
.width('100%')
.onClick(() => {
// 跳转至对应的应用或页面
})
}
// ...
其次,广告SDK创建自定义广告节点控制器。ADNodeController类继承NodeController,用于绑定NodeContainer容器,它持有一个BuilderNode自定义节点,可以挂载原生组件。NodeController可通过BuilderNode持有的FrameNode将其挂载到NodeContainer上。
NodeController类中的函数makeNode函数会在NodeController实例绑定的NodeContainer组件创建的时候回调,回调方法将返回一个节点,将该节点挂载至NodeContainer。
// entry\src\main\ets\pages\ADNodeController.ets
// ...
// 自定义广告节点控制器
class ADNodeController extends NodeController {
private node: BuilderNode<[ADNodeParams]> | null = null;
// 当NodeController绑定的NodeContainer挂载显示时,触发此回调
// 可以加一个打点,记录标明广告被真实的显示出来的时间
aboutToAppear(): void {
console.info('ADController aboutToAppear');
}
// 当NodeController绑定的NodeContainer卸载消失时,触发此回调
// 可以加一个打点,记录标明广告退出的时间
aboutToDisappear(): void {
console.info('ADController aboutToDisappear');
}
// 当NodeController实例绑定的NodeContainer创建的时候进行回调。回调方法将返回一个节点,将该节点挂载至NodeContainer。
makeNode(uiContext: UIContext): FrameNode | null {
this.node = new BuilderNode<[ADNodeParams]>(uiContext);
this.node.build(new WrappedBuilder<[ADNodeParams]>(ADNodeBuilder),
new ADNodeParams('app.media.al_pc', '点击跳转至官网', ''));
return this.node.getFrameNode();
}
// 更新渲染节点
update(params: ADNodeParams) {
if (this.node != null) {
console.info(`update params:${JSON.stringify(params)}`)
this.node.update(params)
}
}
}
应用界面展示广告。创建NodeController广告节点控制器实例,和应用页面广告NodeContainer布局空间进行绑定,完成广告的动态展示。
import { NodeController } from '@kit.ArkUI';
import { ADNodeController } from './ADNodeController';
@Entry
@Component
struct Index {
@State isShow: boolean = true;
private controller: NodeController | null = new ADNodeController();
build() {
Stack() {
if (this.isShow) {
NodeContainer(this.controller)
Button('跳过')
.onClick(() => {
this.isShow = false;
})
} else {
Text('应用首页')
}
}
.alignContent(this.isShow ? Alignment.TopEnd : Alignment.TopStart)
.width('100%')
.height('100%')
}
动态生成页面实践案例
场景描述
下面使用视频首页刷新图片资源作为场景,来介绍如何使用ArkUI的FrameNode来实现动态布局。
ArkUI的声明式扩展使用
一个简化的动态布局类框架的DSL一般会使用JSON、XML等数据交换格式来描述UI,下面使用JSON为例进行说明。本案例相关核心字段含义如下表所示:
标签 | 含义 |
---|---|
type | 描述UI组件的类型,通常与原生组件存在一一对应的关系,也可能是框架基于原生能力封装的某种组件 |
content | 文本,图片类组件的内容 |
css | 描述UI组件的布局特性 |
children | 当前组件的子组件 |
- 定义视频应用首页UI描述数据,在resources/rawfile目录下创建structure.json文件,内容如下。
{
"type": "Column",
"css": {
"width": "100%"
},
"children": [
// ...
]
}
- 定义相应数据结构用于接收UI描述数据,代码示例如下。
export class VM {
type?: string;
content?: string;
css?: ESObject;
children?: VM[];
id?: string;
}
- 自定义DSL解析逻辑,且使用carouselNodes保存轮播图节点,方便后续操作节点更新,代码示例如下。
let carouselNodes: typeNode.Image[] = [];
function frameNodeFactory(vm: VM, context: UIContext): FrameNode | null {
if (vm.type === 'Column') {
let node = typeNode.createNode(context, 'Column');
setColumnNodeAttr(node, vm.css);
vm.children?.forEach(kid => {
let child = frameNodeFactory(kid, context);
node.appendChild(child);
});
return node;
} else if (vm.type === 'Row') {
// ...
} else if (vm.type === 'Swiper') {
// ...
} else if (vm.type === 'Image') {
// ...
} else if (vm.type === 'Text') {
// ...
}
return null;
}
function setColumnNodeAttr(node: typeNode.Column, css: ESObject) {
node.attribute.width(css.width);
node.attribute.height(css.height);
node.attribute.backgroundColor(css.backgroundColor);
if (css.alignItems === 'HorizontalAlign.Start') {
node.attribute.alignItems(HorizontalAlign.Start);
}
}
function setRowNodeAttr(node: typeNode.Row, css: ESObject) {
node.attribute.width(css.width);
if (css.padding !== undefined) {
node.attribute.padding(css.padding as Padding);
}
if (css.margin !== undefined) {
node.attribute.margin(css.margin as Padding);
}
node.attribute.justifyContent(FlexAlign.SpaceBetween);
}
- 使用NodeContainer组件嵌套ArkUI的FrameNode扩展和ArkUI的声明式语法。
class ImperativeController extends NodeController {
makeNode(uiContext: UIContext): FrameNode | null {
return frameNodeFactory(data, uiContext);
}
}
@Component
export struct ImperativePage {
private controller: ImperativeController = new ImperativeController();
build() {
Column() {
NodeContainer(this.controller)
}
.height('100%')
.width('100%')
.backgroundColor(Color.Black)
}
}
性能对比
以场景示例中的两种方案实现,通过DevEco Studio的Profile工具抓取Trace进行性能分析比对。