首页 > 其他分享 >Flutter 陈航 09-视图渲染 三棵树 Widget

Flutter 陈航 09-视图渲染 三棵树 Widget

时间:2022-12-04 19:11:19浏览次数:60  
标签:RenderObject Widget 渲染 09 视图 Element Flutter

本文地址


目录

目录

09 | Widget,构建 Flutter 界面的基石

从 Flutter 官方的架构图可以看出,Widget 是整个视图描述的基础。

那么,Widget 到底是什么呢?

Widget 是 Flutter 开发框架中最基本的概念,是功能的抽象描述,是视图的配置信息,同样也是数据的映射

前端框架中常见的名词,比如 View、View Controller、Activity、Application、Layout 等,在 Flutter 中都是 Widget。

事实上,Flutter 的核心设计思想便是“一切皆 Widget”。

视图渲染过程

在进行 App 开发时,我们往往会关注的一个问题是:如何结构化地组织视图数据,提供给渲染引擎,最终完成界面显示。

各种 UI 框架无一例外地都会用到视图树(View Tree)的概念。而 Flutter 将视图树的概念进行了扩展,把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。

这三部分之间的关系,如下所示:

Widget

Widget 是 Flutter 世界里对视图的一种结构化描述,你可以把它看作是前端中的“控件”或“组件”。Widget 是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。

Flutter 将 Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新。

但,这样做的缺点是,因为涉及到大量对象的销毁和重建,所以会对垃圾回收造成压力。不过,Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建的成本很低。

得益于 Widget 的不可变性,可以以较低成本进行渲染节点复用,因此在一个真实的渲染树中可能存在不同的 Widget 对应同一个渲染节点的情况,这无疑又降低了重建 UI 的成本。

问:什么叫渲染节点?为何不可变性可以进行渲染节点复用?
答:渲染节点可以认为是下文提到的 Element;如果 widget 可变,但是内存地址不变,就很难比对新旧状态的差异

Element

Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息(即 Widget)到完成最终渲染的桥梁

问:Element 是 Widget 的一个实例化对象,是什么含义?
答:表示 Widget 是一个配置,Element 才是最终的对象;Element 是通过遍历 Widget 树时,调用 Widget 的方法创建的

Flutter 渲染过程,可以分为这么三步:

  • 首先,通过 Widget 树生成对应的 Element 树
  • 然后,创建相应的 RenderObject 并关联到 Element.renderObject 属性上
  • 最后,构建成 RenderObject 树,以完成最终的渲染

可以看到,Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。那你可能会问,既然都是发号施令,那为什么需要增加中间的这层 Element 树呢?直接由 Widget 命令 RenderObject 去干活儿不好吗?

答案是,可以,但这样做会极大地增加渲染带来的性能损耗。

因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(diff)做了抽象,可以只将真正需要修改的部分同步到 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

这,就是 Element 树存在的意义。

Element 是可复用的,只要 Widget 前后类型一样。比如 Widget 是蓝色的,重建后变红色了,Element 是会复用的。所以多个 Widget(销毁前后)可以对应一个 Element

RenderObject

从其名字,我们就可以很直观地知道,RenderObject 是主要负责实现视图渲染的对象。

Flutter 通过控件树(Widget 树)中的每个控件(Widget)创建不同类型的渲染对象,组成渲染对象树。

渲染对象树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。

Flutter 通过引入 Widget、Element 与 RenderObject 这三个概念,把原本从视图数据到视图渲染的复杂构建过程拆分得更简单、直接,在易于集中治理的同时,保证了较高的渲染效率。

RenderObjectWidget 介绍

在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。

abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement(); // 创建 Element

  @protected
  RenderObject createRenderObject(BuildContext context); // 创建 RenderObject

  @protected
  void updateRenderObject(BuildContext c, covariant RenderObject ro) { } // 更新 RenderObject
}

RenderObjectWidget 中同时拥有创建 Element、RenderObject,以及更新 RenderObject 的方法。

但实际上,RenderObjectWidget 本身并不负责这些对象的创建与更新,它只是提供了创建和更新的方法,可以让框架在合适的时机调用。

Element 的创建

对于 Element 的创建,Flutter 会在遍历 Widget 树时,调用 createElement() 去同步 Widget 自身配置,从而生成对应节点的 Element 对象。

RenderObject 的创建与更新

对于 RenderObject 的创建与更新,其实是在 RenderObjectElement 类中完成的。

abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = (widget as RenderObjectWidget).createRenderObject(this); // 创建 RenderObject
    attachRenderObject(newSlot);
    _dirty = false;
  }

  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    (widget as RenderObjectWidget).updateRenderObject(this, renderObject); // 更新 RenderObject
    _dirty = false;
  }
}

在 Element 创建完毕后,Flutter 会调用 Element 的 mount 方法。在这个方法里,会完成与之关联的 RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。

如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。

而真正的绘制和布局过程,则完全交由 RenderObject 完成:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  void layout(Constraints constraints, { bool parentUsesSize = false }) {}
  void paint(PaintingContext context, Offset offset) {}
}

布局和绘制完成后,接下来的事情就交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。

案例分析

在下面的例子中,一个 Row 容器放置了 4 个子 Widget,左边是 Image,而右边则是一个 Column 容器下排布的两个 Text。

那么,在 Flutter 遍历完 Widget 树,创建了各个子 Widget 对应的 Element 的同时,也创建了与之关联的、负责实际布局和绘制的 RenderObject。

示例界面生成的“三棵树”

总结

Flutter 中视图数据的组织和渲染抽象的三个核心概念:

  • Widget 是 Flutter 世界里对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息
  • Element 是 Widget 的一个实例化对象,将 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真实的 Render Object 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程
  • RenderObject 则负责实现视图的最终呈现,通过布局、绘制完成界面的展示

如果用 Vue 来比喻的话,Widget 就是 Vue 的 template,Element 就是 virtual DOM,RenderObject 就是DOM。

  • React:JSX -> 虚拟DOM -> 浏览器DOM
  • React Native:JSX -> 虚拟DOM -> Android/iOS原生控件
  • Flutter:Widget -> Element(类似虚拟DOM,只是一种数据结构)-> RenderObject 交给底层渲染

在日常开发学习中,绝大多数情况下,我们只需要了解各种 Widget 特性及使用方法,而无需关心 Element 及 RenderObject。因为 Flutter 已经帮我们做了大量优化工作,因此我们只需要在上层代码完成各类 Widget 的组装配置,其他的事情完全交给 Flutter 就可以了。

2022-12-4

标签:RenderObject,Widget,渲染,09,视图,Element,Flutter
From: https://www.cnblogs.com/baiqiantao/p/16950439.html

相关文章

  • 2022-2023-1 20221409 《计算机基础与程序设计》第十四周学习总结
    2022-2023-120221409《计算机基础与程序设计》第十四周学习总结作业信息这个作业属于哪个课程2022-2023-1-计算机基础与程序设计这个作业要求在哪里如2022-2......
  • MySQL进阶实战6,缓存表、视图、计数器表
    一、缓存表和汇总表有时提升性能最好的方法是在同一张表中保存衍生的冗余数据,有时候还需要创建一张完全独立的汇总表或缓存表。缓存表用来存储那些获取很简单,但速度较慢......
  • CF1709 题解
    比赛链接:https://codeforces.com/contest/1709题解:AB水题//bySkyRainWind#include<cstdio>#include<vector>#include<cassert>#include<cstring>#include<......
  • 顺序表-00009-头插法与尾插法
    顺序表结构定义typedefintseqType; //定义顺序表数据类型//定义顺序表的结构体typedefstructt_sList{ seqType*pbase; //表基址 intcapacity; //表......
  • DRF-视图
    DRF视图类中除了APIView类,还有一个GenericAPIView类。GenericAPIView类主要给我们提供了2个属性,3个方法。属性:queryset:要序列化的数据serializer_class:序列化类 方法......
  • 09watch(监视属性)对比computed(计算属性)
    一、监视属性与计算属性普通实现对比二、监视属性与计算属性异步实现对比三、对比computed和watch之间的对比:1.computed能完成的给你,watch都能完成;2.watch能......
  • 力扣09 判断一个数是否是回文数
    力扣09判断一个数是否是回文数题目:给你一个整数x,如果x是一个回文整数,返回true;否则,返回false。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如......
  • Treewidget 节点的遍历
    父节点的遍历  //Treewidget遍历操作  //只遍历父节点   intnParentNodeCount=ui->treeWidget->topLevelItemCount();   for(intnIndex=0;nInd......
  • Treewidget节点的增加
    父节点的创建   //隐藏QTreewidget标题头   ui->treeWidget->header()->hide();   //实现Treewidget父节点的挂载   //创建存放QTreewidget的容器......
  • Treewidget节点的删除
    父节点的删除    //第一种   //树状列表父节点的删除   //有点莽不支持这种操作deleteui->treeWidget->topLevelItem(0);    // 第二种 ......