鸿蒙系统进一步学习(三):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小结与思考对上述几棵树的功能做个小结三棵树分别承担着不同的责任1Component树每次创建或者更新时都会重新生成相应的子树结构成员方法提供了创建Element和Render节点成员变量保存相应的属性值。2Element树维持UI组件树形结构承载计算树之间差异的任务在新的Component子树生成并请求更新时会基于老的树形结构进行差异计算来实现树形结构更新和渲染节点属性更新。3Render树承载布局渲染任务保存Component结构中的属性值基于保存的属性值驱动内容布局和渲染。进一步思考然而读者也应该有一个感觉这三棵树的渲染机制虽然能解决最小化刷新问题但是也会带来一些缺陷1对于每个页面都有三棵树会带来额外的内存的开销其中Component树和Element树高度相似略有冗余。2在列表滑动等实时性要求严苛的场景下如快速滑动列表这也是自媒体非常喜欢测试流畅度的场景之一列表动态创建相应的列表项时需要创建更多的对象Component、Element和Render同时属性值也需要进行多次拷贝赋值属性值在创建Component组件时会先复制到Component内的成员中在Element和Render节点创建完成后上述的属性值会再次拷贝到Render节点中以便进行内容布局和绘制复杂场景下可能带来帧率的下降。所以如何砍掉多余的树也同时能够保持最小化刷新将是要考虑和解决的难题