当前位置: 首页 > news >正文

鸿蒙系统进一步学习(三):ArkUI的差分渲染

1. UI组件树

以一段普通的代码为例,里面有个空布局,它的长宽都为父控件的80%,并设置了背景色。经过编译运行后,通过ArkUI的载入后,它的内部数据结构如图1所示。

@Entry @Component struct Index { build() { Stack() { }.width('80%').height('80%') .backgroundColor('#fff111') } }

图1 UI组件树示意图

最上面的一层是根节点,所有系统组件的节点的数据结构都是FrameNode,第二层也是个FrameNode,但是整个Page的根节点,一个Ability有不同的Page组成,当不同的Page载入的时候,实际就是把自己挂在根节点下,然后渲染,这样会变成为新的页面,第三个节点则是CustomNode,即用户定义的节点,这个时候刻意看到@Entry关键字了,此时读者应该明白了,这个就是Index节点,Index下有个Stack组件。所以真正意思上来说,@Entry节点只是第三层的节点,它的上面还有两个节点,一个是根节点,当应用启动时,ArkUI框架会初始化应用UI根节点;另外一个就是Page的节点,表示当前应用加载的是哪个页面。

2. 三棵树

为了实现局部的最小化更新,老版本的ArkUI定义了三棵树模型,这三棵树分别是Component、Element和Render,它们的组合实现了数据驱动UI的最小化更新。它的核心思路是:在状态发生变化的时候,比对Element树和Component树的差异点,形成差异树,在差异树中,选取可以渲染的节点,生成Render树,最后把Render树交给渲染管线去渲染。

在UI首次创建时,会先生成Component树,基于Component树会生成最初的Element树和Render树,Render树在生成时会忽略非渲染节点对象(比如自定义的Component,本身没有显示内容,不参与渲染),这些对象会生成页面唯一的id标识,用于后续的更新。

1)树的生成

一段有@Component注解的代码,在经过编译后,最后到ArkUI后,首次会生成一颗树形结构,这个就是Component树,基于Component树,复刻一个树,就是Element树。记住,这是首次创建的时候,如下代码就会生成一颗如图2的一棵树。

@Component struct MyComponent { @State needShowSecond: boolean = true @State message: string = "hello world" build() { Column() { Text(this.message) if (this.needShowSecond) { Text(this.message) } } } }

图2 MyComponent生成的Component树

可以看出,needShowSecond这个变量此时是true,所以if判断条件成立,Column容器下有两个子节点,其中右边的子节点是依据该变量从而决定是否显示。同时可以看出,build()函数的里面的层级决定了树的深度,所以在了解清楚这个原理后,读者在后期的开发中,一定要能尽量降低层级,减少树的深度,从而提高树的遍历效率,这样后期的渲染会更加高效。

上述只是一个简单的自定义组件的树生成情况,那么带到一个完整的页面中,除了要生成树的结构之外,还需要对每一个节点做Id的标记,这样每个节点可以唯一被表示。从UI根节点开始,完整的生成的Component树如图3所示。同时在ArkUI还会克隆出一颗一模一样的Element树,从标号1开始,就是build()里面的Column容器,每深入一个层级,标号就多一位,兄弟层级之间,尾号加1,为了后面方便讲解。后续的节点树都从build()函数的第一个容器开始,省去根节点到@Entry节点,它们不是消失了,只是不展示,因为同一个页面的刷新,不涉及到它们,减少整体展示图的大小。

@Component struct MyComponent { @State needShowSecond: boolean = true @State message: string = "hello world" build() { Column() { Text(this.message).onClick(() => { this.needShowSecond = !this.needShowSecond }) if (this.needShowSecond) { Text(this.message) } } } } @Entry @Component struct Index { @State message: string = 'Hello World' build() { Column() { Row() { Stack() { Text(this.message) } } MyComponent() } } }

图3 Index页面的完整树形图

那么第三棵树,渲染树是怎么样的呢?之前提过,渲染树会删除一些非渲染的节点,也就是1-2编号的节点,它的作用是支撑起完整的树形结构,在ArkUI里面,这样的节点称为Composed对象(非渲染节点),Composed对象会生成页面唯一的id标识,用于后续的更新。当1-2节点因为是Composed对象,所以被渲染树移除后,原来的1-2-1节点,也就是MyCompoent里面的Column节点,它的层级就会上升,成为渲染树中的1-2节点,如图4所示。至此三棵树在初始阶段创建完成。

图4 Index页面创建时所对应的渲染树

2)树的更新

首次创建的时候,树一定是全量的,如果此时点击了MyComponent里面的第一个Text,就会把needShowSecond取反,因为@State的变化了,就会触发MyComponent的刷新,此时Component树的结构发生了变化,1-2-1-2节点被删除了,它就会和原来的旧的Element树进行比较,生成新的Render树,此时可以看到,Render树比较小,只有两个节点要更新,那么此时只需要对这两个节点渲染即可,其它节点不需要动,从而实现了最小化刷新。同步地,旧的Element树也更新为新的Element树了。其流程示意如图5所示。

图5 三颗树的更新流程示意图

纵观整个例子,读者可以发现,整个的思路是非常清晰的,即不断比对新旧两棵树的差异,生成渲染树,最后交给渲染管线去渲染即可。

3)小结与思考

对上述几棵树的功能做个小结,三棵树分别承担着不同的责任:

(1)Component树:

每次创建或者更新时都会重新生成相应的子树结构,成员方法提供了创建Element和Render节点,成员变量保存相应的属性值。

(2)Element树:

维持UI组件树形结构,承载计算树之间差异的任务,在新的Component子树生成并请求更新时,会基于老的树形结构进行差异计算,来实现树形结构更新和渲染节点属性更新。

(3)Render树:

承载布局渲染任务,保存Component结构中的属性值,基于保存的属性值驱动内容布局和渲染。

进一步思考:

然而,读者也应该有一个感觉,这三棵树的渲染机制,虽然能解决最小化刷新问题,但是也会带来一些缺陷:

(1)对于每个页面,都有三棵树,会带来额外的内存的开销,其中Component树和Element树高度相似,略有冗余。

(2)在列表滑动等实时性要求严苛的场景下(如快速滑动列表,这也是自媒体非常喜欢测试流畅度的场景之一),列表动态创建相应的列表项时需要创建更多的对象(Component、Element和Render,同时属性值也需要进行多次拷贝赋值,属性值在创建Component组件时会先复制到Component内的成员中,在Element和Render节点创建完成后,上述的属性值会再次拷贝到Render节点中以便进行内容布局和绘制),复杂场景下可能带来帧率的下降。

所以如何砍掉多余的树,也同时能够保持最小化刷新,将是要考虑和解决的难题!

http://www.jsqmd.com/news/1099520/

相关文章:

  • 3D CAD SDK 安装
  • Spring AI + RAG
  • 大模型服务弹性伸缩:从 GPU 利用率到 K8s HPA 的全链路实战
  • 告别Keil律师函!手把手教你用VSCode+GNU Arm+STM32CubeMX搭建免费单片机开发环境(Windows版)
  • 从零到一:基于Dify的AI应用开发全流程实践指南
  • 气泡特效的核心在于BubbleEffect类,它继承自Manim的Animation类,通过重写关键方法来实现气泡的上升、变大和透明度变化效果。
  • 操作系统缓存机制深度解析:从页缓存到内存映射,超越Redis的性能优化之道
  • 深智微:华润微官方授权代理商,如何让型号、库存交期与项目交付协同推进
  • 新用户福利,千问新用户福利怎么领,领取8元优惠券,附最新口令
  • Redis缓存:Python操作Redis实战
  • 行测申论试卷|公务员申论刷题|数量关系备考
  • 2026年人工智能发展的新风口:从“数字智能”到“物理智能”的范式革命
  • Qt——文本绘制技巧
  • 学习一门语言——Python(自学版)
  • 你的输出第一行肯定和我不一样,且每次运行的输出肯定不一样。
  • 乳牙蛀了不用管?避开护牙误区,科学守护孩子恒牙健康
  • info = {“k1“:1,“k2“:“222“,“k3“:“guohan“} 可变类型不可以做字典的键(列表,字典,集合) 因此键可以是整数什么的如 info = {1:2}公
  • 闪迪 x300 2.5-sata
  • 通用二进制协议分析:基于AST的Protobuf动态解析与容错设计
  • — 一款针对 IT 团队开发的文档管理系统,基于 Golang 开发,内置项目管理,用户管理,权限管理等功能,可以用来储存日常接口文档,数据库字典,手册说明等文档。功能特色:
  • 零基础使用VMware虚拟机安装CentOS Linux超详细教程
  • 硬件学习第三天(阅读芯片手册)
  • c语言项目驱动学习--实例化(图书管理)--005-代码对比
  • 基于51单片机RFID车位车库管理系统/RC522读卡/车库收费系统2(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)
  • [智能体-612]:AGENTS.md 完整详解(OpenClaw 体系,对应 workspace 里的工作规范文件)
  • 技术速递|通过全新开放数据集,加速研究人员和开发者构建多语言 AI
  • 中餐厅摆台 点击UI图片拖拽预制体到桌面并 实现预制体拖拽
  • 家庭精细化洗护怎么选?海信全家筒·棉花糖 U7S 四筒洗衣机深度体验
  • MHmarkets:把产品理解成本做扎实,更谨慎的使用者更容易感受到的清单
  • 基于STM32单片机甲醛温湿度烟雾火灾报警 空气质量检测PM2系统2(设计源文件+万字报告+讲解)(支持资料、图片参考_降重降ai)