Flutter 08 三棵树(Widgets、Elements和RenderObjects)

一、Flutter三棵树背景

1.1 先思考一些问题

1. Widget与Element是什么关系?它们是一一对应的还是怎么理解?

2. createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?

3. Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?

4. Widget、Element、RenderObject 三棵树之间的关系是怎样的?

1.2 Flutter中Dom树

如何理解 DOM 树这个概念

它由页面中每一个控件组成,这些控件所形成的一种天然的嵌套关系使其可以表示为 “树” 结构,可以将 这个概念应用在 Flutter 中。

例如默认的计数器应用的结构如下图:

二、Flutter中的三棵树 

即Widget树、Element树和RenderObject树。

Widget树:控件的配置信息,不涉及渲染,更新代价极低。

RenderObject树:真正的UI渲染树,负责渲染UI,更新代价极大。

Element树:Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到 RenderObject树上。

Widget 树

我们平时用 Widget 使用声明式的形式写出来的界面,可以理解为 Widget 树,这是要介绍的第一棵树。 Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕 上的显示元素,而它只是描述显示元素的一个配置数据。

RenderObject 树

Flutter 引擎需要把我们写的 Widget 树的信息都渲染到界面上,这样人眼才能看到,跟渲染有关的当然 有一颗渲染树 RenderObject tree,这是第二颗树,渲染树节点叫做 RenderObject,这个节点里面处理 布局、绘制相关的事情。

这两个树的节点并不是一一对应的关系,有些 Widget是要显示的,有些 Widget ,比如那些继承自 StatelessWidget & StatefulWidget 的 Widget 只是将其他 Widget 做一个组合,这些 Widget 本身并不 需要显示,因此在 RenderObject 树上并没有相对应的节点。

Element 树

Widget 树是非常不稳定的,动不动就执行 build方法,一旦调用 build 方法意味着这个 Widget 依赖的 所有其他 Widget 都会重新创建,如果 Flutter 直接解析 Widget树,将其转化为 RenderObject 树来直 接进行渲染,那么将会是一个非常消耗性能的过程,那对应的肯定有一个东西来消化这些变化中的不 便,来做cache。

因此,这里就有另外一棵树 Element 树。Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低 对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

三、Flutter三棵树关系

三棵树架构关系

三棵树架构图:

总结的关系:

widget 树和 Element 树节点是一一对应关系,每一个 Widget 都会有其对应的 Element,但是 RenderObject 树则不然,只有需要渲染的 Widget 才会有对应的节点。

Element 树相当于一个中间层,大管家,它对 Widget 和 RenderObject 都有引用。

当 Widget 不断变化的时候,将新 Widget 拿到 Element 来进行对比,看一下和之前保留的 Widget 类 型和 Key 是否相同,如果都一样,那完全没有必要重新创建 Element 和 RenderObject,只需要更新里 面的一些属性即可,这样可以以最小的开销更新 RenderObject,引擎在解析 RenderObject 的时候,发 现只有属性修改了,那么也可以以最小的开销来做渲染。

简单总结一下:

Widget 树就是配置信息的树,我们平时写代码写的就是这棵树。

RenderObject 树是渲染树,负责计算布局,绘制,Flutter 引擎就是根据这棵树来进行渲染的。

Element 树作为中间者,管理着将 Widget 生成 RenderObject和一些更新操作。

举个通俗例子:

UI 渲染就像盖一栋大楼,Widget 代表图纸,表示我们想造怎样的大楼,RenderObject 是根据图纸干活 的工人,而 Element 是监工,负责协调各方资源,统一调配,外部人员有事需要先找这个监工。 

三者创建关系图:

用文字描述三者创建关系

首先是 Widget 通过调用其 createElement 方法创建出 Element 对象。

Element 继续调用其持有 Widget 对象(Stateless)或 State 对象(Stateful)的 build 方法创建其子 widget 对象。往复循环,继续创建子Element,子 Element 持有父 Element 的引用,因此最终会形成 出一颗 Element 树。

对于有 layout/paint 的能力控件,会创建 RenderObjectElement,在该 Element 的 mount 阶段会创 建其对应的 RenderObject 对象。 

四、运行时三棵树结构

三棵树结构

认识了三棵树之后,那Flutter是如何创建布局的?以及三棵树之间他们是如何协同的呢?

接下来就让我们通过一个简单的例子来剖析下它们内在的协同关系:

class Tree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.brown,
      child: Row(
        children: [
          new Image.network(
            "https://p1.ssl.qhmsg.com/dr/220__/t01d5ccfbf9d4500c75.jpg",
            width: 100,
            height: 100,
          ),
          new Text(
            "从网络加载图片",
            style: TextStyle(fontSize: 16),
          ),
        ],
      ),
    );
  }
}

当runApp()被调用时,第一时间会在后台发生以下事件:

1.Flutter会构建包含Widget(Container,Row,Image,Text)的Widgets树;

2.Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对 象,最后将这些对象组建成Element树;

3.接下来会创建第三个树,这个树中包含了与Widget对应的Element通过createRenderObject() 创建的RenderObject;

具体Flutter经过这三个步骤后的状态:

总结一下三棵树结构:

1. Widget Tree: Widget 是 Flutter 面向开发者的上层接口,我们通过 widget 的层层嵌套,会形成 一颗 Widget 树,一个 Widget 可在多个位置复用。Flutter Framework 层为我们提供了一些常用 的包装或者容器的 Widget,比如 Container,其内部继续嵌套了其他 Widget,如 Padding、Align 等等。所以,开发者编写的 Widget 树和实际生成的 Widget 树都会略有差别。如图中虚线圆形标 注的 ColorBox、RawImage 等。

2. Element Tree :每一个 Widget 都会对应一个 Element,只不过 Element 分类不同。

3. RenderObject Tree:RenderObject 只负责最终的测量、布局和绘制,因此最终的 RenderObject Tree 是 Element Tree 剔除掉哪些包装,最后组织而成的 Tree。

为何搞这多树?

分层:开发只关注widget

1. Framework 将复杂的内部设计、渲染逻辑与开发接口隔离开,应用层只需关注 Widget 开发即 可。

高效:提交绘制效率

1. Tree 最大的共同特点就是快取,因为 Element、RenderObject 销毁重建成本很高,一旦可以复用 ,那么快取可以大幅减少这种开销。

2. 比如:当 Element 不需要重建时,更新 Widget 的引用就可以了;Layer Tree 的设计是将绘制图层 分开,方便提取和合成,合成层中的 transform 和 opacity 效果,都只是几何变换、透明度变换 等,不会触发 layout 和 paint,直接由 GPU 完成即可。

五、三棵树的作用介绍

1. 简而言之是为了性能,为了复用Element从而减少频繁创建和销毁RenderObject。

2. 因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对性能的影响 比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的 Widget树:

//framework.dart
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if (child != null) {
    assert(() {
      final int oldElementClass = Element._debugConcreteSubtype(child);
      final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
      hasSameSuperclass = oldElementClass == newWidgetClass;
      return true;
    }());
    if (hasSameSuperclass && child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true;
      }());
      newChild = child;
    } else {
      deactivateChild(child);
      assert(child._parent == null);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
    newChild = inflateWidget(newWidget, newSlot);
  }
  assert(() {
    if (child != null)
      _debugRemoveGlobalKeyReservation(child);
    final Key key = newWidget?.key;
    if (key is GlobalKey) {
      key._debugReserveFor(this, newChild);
    }
    return true;
  }());
  return newChild;
}

//...

static bool canUpdate (Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
  && oldWidget.key == newWidget.key;
}

1. 如果某一个位置的Widget和新Widget不一致,才需要重新创建Element;

2. 如果某一个位置的Widget和新Widget一致时(两个widget相等或runtimeType与key相等),则只需 要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了;

3. 因为Widget是非常轻量级的,实例化耗费的性能很少,所以它是描述APP的状态(也就是 configuration)的最好工具;

4. 重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用; 

更新时三棵树操作

因为Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都需要被重建。

1. 例如当我们改变一个Text文本的时候,框架就会触发一个重建整个Widget树的动作。

2. 因为有了Element的存在,Flutter会比较新的Widget树中的第一个Widget和之前的 Widget。

3. 接下来比较Widget树中之后Widget和之前Widget,以此类推,直到Widget树比较完成。

@override
Widget build(BuildContext context) {
  return Container(
    color: Colors.brown,
    height: double.infinity,
    child: Row(
      children: [
        new Image.network(
          "https://p1.ssl.qhmsg.com/dr/220__/t01d5ccfbf9d4500c75.jpg",
          width: 100,
          height: 100,
        ),
        new Text(
          "改变UI",
          style: TextStyle(
              fontSize: 16
          ),
        ),
      ],
    ),
  );
}

Flutter遵循一个最基本的原则:

判断新的Widget和老的Widget是否是同一个类型:

如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子树)上 移除,然后创建新的对象;

如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/121863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VUE页面导出PDF方案

1,技术方案为:html2canvas把页面生成canvas图片,再通过jspdf生成PDF文件; 2,安装依赖: npm i html2canvas -S npm i jspdf -S 3,封装导出pdf方法exportPdf.js: // 页面导出为pdf格式 //titl…

MAC设备(M1)环境下编译安装openCV for Java

最近发现一个需求,可以用openCV来实现,碰巧又新买了mac笔记本,就打算利用业余时间安装下openCV。这里将主要步骤记录下,希望能帮助有需要的人。 1、准备编译环境 #查询编译opencv相关依赖 brew info opencv查询结果如下图所示&a…

TSN技术:智慧交通系统的时钟同步与流量调度利器

智慧交通是光路科技战略规划的核心领域之一。自2022年以来,光路科技推出了满足“确定性传输”、“低延时”和“高可靠”需求的TSN时钟敏感网络交换机产品服务,旨在构建智慧交通网络通信的高效、高速、高标准的稳定系统。 在智慧交通系统中,路…

电脑上使用的待办事项便签工具选择哪一款?

电脑是日常办公中必不可少的工具,每一个人每天都要面对纷纷总总的工作待办任务,倘若能当日常工作待办逐一罗列好,提前制定好相关的工作计划后,就可以让各项工作有据可循,在电脑上有哪些可以记录待办事项的工具能实时起…

合并两个有序链表OJ

合并两个有序链表OJ 文章目录 合并两个有序链表OJ一、题目及要求二、思路分析三、代码实现 一、题目及要求 二、思路分析 其次,题目里说了新链表是通过拼接原来的结点形成的,所以说我们不需要开辟新的空间。 三、代码实现 if (list1 NULL) {return li…

【JavaEESpring】Spring, Spring Boot 和Spring MVC的关系以及区别

Spring, Spring Boot 和Spring MVC的关系以及区别 Spring:简单来说,Spring 是一个开发应用框架,什么样的框架呢?轻量级、一站式、模块化,其目的是用于简化企业级应用程序开发 Spring的主要功能: 管理对象, 以及对象之…

基于RK3568的内部定时器应用示例

1. 内部定时器介绍 内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于kernel/linux/timer.h和kernel/timer.c 文件中。 被调度的函数肯定是异步执行的,它类似于一种“软件中断”&am…

docker influxdb

docker & influxdb 搜索镜像 docker search influxdb docker pull influxdb: 1.4.2 docker run -d -p 8086:8086 --name influxdb influxdb:1.4.2 docker exec -it influxdb bash 连接influxdb 控制台 influx -host localhost -port 8086 influx -username root -passw…

数据结构与算法C语言版学习笔记(2)-线性表、顺序存储结构的线性表

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 数据结构部分的知识框架一、线性表的定义和特点1.定义2.特点 二、线性表的实际案例引入1.案例一:多项式的加减乘除2.案例二:当多项式是稀疏多…

Flutter 07 框架和三棵树(Widgets、Elements和RenderObjects)

一、Flutter框架的整体结构: Flutter是Google推出并开源的跨平台开发框架,主打跨平台、高保真、高性能。开发者可以通过Dart语 言开发Flutter应用,一套代码同时运行在ios和Android平台。不仅如此,Flutter还支持Web、桌面、嵌 入应…

打卡系统有什么用?如何通过日常管理系统提高企业员工的效率?

在当今快速发展的时代,职工的行动管理已成为机构单位工作中至关重要的一环。如何更好地了解和掌握职工的日程安排和行动轨迹,成为了一个值得探讨的问题。为了解决这一难题,“的修”打卡系统应运而生,它为我们提供了一个全面而高效…

智能AI系统ChatGPT系统源码+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统,支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美,可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

73.矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1: 输入:matrix [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]]示例 2: 输入&…

leetcode链表

这几天手的骨裂稍微好一点了,但是还是很疼,最近学校的课是真多,我都没时间做自己的事,但是好在今天下午是没有课的,我也终于可以做自己的事情了。 今天分享几道题目 移除链表元素 这道题我们将以两种方法开解决&…

python特殊循环队列_队中元素个数代替队尾指针

对于循环队列来说,如果知道队头指针和队中元素个数,则可以计算出队尾指针。也就是说,可以用队中元素个数代替队尾指针。设计出这种循环队列的判队空、进队、出队和取队头元素的算法。 本例的循环队列包含data 数组、队头指针 front和队中元素…

基于FPGA的图像RGB转HSV实现,包含testbench和MATLAB辅助验证程序

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1. RGB与HSV色彩空间 4.2. RGB到HSV转换原理 5.算法完整程序工程 1.算法运行效果图预览 将FPGA的仿真结果导入到matlab中: 2.算法运行软件版本 vivado2019.2 matlab2022a …

【漏洞复现】Viessmann Vitogate远程代码执行(CVE-2023-45852)

漏洞描述 Vitogate 300 2.1.3.0版本的/cgi-bin/vitogate.cgi存在一个未经身份验证的攻击者可利用的漏洞,通过put方法中的ipaddr params JSON数据中的shell元字符实现绕过身份验证并执行任意命令。 免责声明 技术文章仅供参考,任何个人和组织使用网络…

低压备自投装置在河北洞庭变电站工程中的应用

安科瑞 崔丽洁 摘 要 随着电力需求的不断增加,电力系统供电可靠性要求越来越高,许多供电系统已具备两回或多回供电线路。备用电源自动投入装置可以*效提高供电的可靠性,该类装置能够在工作电源因故障断开后,自动且迅速地将备用电源…

在Windows或Mac上安装并运行LLAMA2

LLAMA2在不同系统上运行的结果 LLAMA2 在windows 上运行的结果 LLAMA2 在Mac上运行的结果 安装Llama2的不同方法 方法一: 编译 llama.cpp 克隆 llama.cpp git clone https://github.com/ggerganov/llama.cpp.git 通过conda 创建或者venv. 下面是通过conda 创建…

K8S篇之K8S详解

一、K8S简介 k8s全称kubernetes,是为容器服务而生的一个可移植容器的编排管理工具。k8s目前已经主导了云业务流程,推动了微服务架构等热门技术的普及和落地。 k8s是自动化容器操作的开源平台。这些容器操作包括:部署、调度和节点集群间扩展。…