0%

Flutter中的树结构浅析

目录

1、Widget、Element、RenderObject

2、Flutter中树结构的创建、更新、复用

3、DOM、Virtual DOM、diff

4、Key的分类及使用

5、参考资料


1、Widget、Element、RenderObject

Widget

  • Flutter的基石,用户界面的不可变的一种描述;会频繁创建、销毁。
  • 作用类似于HTML中的标签。

image.png

Element

  • Widget的实例化对象,在树中详细的位置。
  • 利用Widget作为配置,用于管理Widget的生命周期、UI的更新。

image.png

RenderObject

  • 由Element的子类RenderObjectElement创建,负责UI的布局、绘制、事件响应。
  • 开发复杂视图时,可以自定义绘制功能。

image.png

Widget、Element、RenderObject之间的关系

  • Element同时持有对Widget和RenderObject的引用。
  • 源码如图所示:

image.png


2、Flutter中树结构的创建、更新、复用

Flutter中的树结构

image.png

为什么创建3棵树?

  • 提高性能
    1
    2
    尽可能的复用Element和RenderObject,因为Widget可能会频繁的创建销毁,
    因此WidgetTree是非常不稳定的,如果每次直接根据WidgetTree重新创建RenderObjectTree会极大的消耗性能。
  • 便于访问状态、树节点之间的结构信息等数据
    1
    2
    3
    例如:
    StatelessElement内部存储了_widget、 _renderObject;
    对于StatefulElement其内部还存储了_state信息。

树的创建过程

整体流程图

image.png

1、Flutter主入口,main()函数。

image.png

2、初始化Flutter功能组件。

image.png

3.1、创建根Widget(即RenderObjectToWidgetAdapter)

  • 内部创建了RenderObjectToWidgetAdapter ,并将我们传入的自定义Widget(即runApp)做为其child;
  • RenderObjectToWidgetAdapter本身是一个RenderObjectWidget,是RenderObject和Element之间的桥梁。

image.png

3.2、创建根Element(即RenderObjectToWidgetElement)

  • 接着执行attachToRenderTree()方法,创建根Element,并调用mount()方法,继续创建根RenderObject。

image.png

3.3、创建根RenderObject

  • 在根Element中调用了mount()方法后,会调用super.mount()方法,即RenderObjectElement.mount(),创建根RenderObject,并将其挂载。

image.png

4、调用scheduleWarmUpFrame方法。

  • 此时WidgetTree、ElementTree、RenderObjectTree对应的结构都已经初步建立,Flutter准备界面渲染和显示。

image.png

Flutter中树的更新

更新规则

image.png

问题:在更新过程中,如何知道Widget能够复用Element呢?

答案:Widget提供了一个核心方法canUpdate,如源码所示:

image.png

  • 默认情况下:Widget的Key == null。
1
当没有给Widget设置Key的时候,Flutter会根据Widget的runtimeType和显示顺序是否相同来判断Widget是否有变化。(runtimeType即Widget的类型)
  • 当给Widget设置了Key时。
1
当给Widget设置了Key时,Flutter根据runtimeType和Key两个条件来判断Widget是否有变化。

Flutter中树的复用


3、DOM、Virtual DOM、diff

DOM是什么?

  • 文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展置标语言的标准编程接口。
  • DOM提供了对整个文档的访问模型,将文档抽象成一个倒立生长的树形结构,树的每个节点表示了一种对象类型,节点之间存在父子、兄弟关系。
  • DOM是一种与平台和语言无关的应用程序接口(API),可以动态地访问程序和脚本,更新其内容、结构。
  • 常见的形式主要有:HTML-DOM、XML-DOM。

常见的HTML-DOM结构

image.png

Virtual DOM

  • Flutter的很多灵感来自于React,比如: Virtual DOM、diff算法、状态管理等。

  • Virtual DOM的本质

1
2
3
对应于Flutter,就是在Widget和RenderObject之间做了一个缓存。
可以类比CPU和硬盘,硬盘速度很慢,所以在两者之间加入内存缓存。
既然直接操纵RenderObject很慢并且消耗性能,就可以在Widget和RenderObject之间加个缓存(Element)。
  • Virtual DOM算法主要步骤
1
2
3
1、用对象结构作为DOM树结构的映射,然后用这个树构建一个真正的DOM树,插到上下文中。
2、当状态发生变更时,重新构造一颗新的对象树,然后用新树和旧树进行比较,记录两棵树的差异,即diff的过程。
3、把第2步所记录的差异,应用到真正的DOM树上(即patch),进行视图更新。

diff

  • diff作为Virtual DOM算法的核心,具有一定的规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、深度优先遍历新旧2棵Virtual DOM树,给每个节点设定唯一的标记。
遍历的同时,将旧树的节点与新树的节点进行比较,将差异依次记录到一个对象中。

例如:
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的差异

2、定义差异类型。
2.1、替换掉原来的节点。例如:把Row、改为Column。
2.2、移动、删除、新增子节点。例如:把多子Widget的children换位置。
2.3、修改了节点的属性。例如:颜色变化。
2.4、文本节点的内容修改。例如:Text(“123”) 改为 Text(“ABC”)。

3、把差异应用到真正的DOM树上,即patch的过程。
对应Flutter中,也就是更新RenderObjectTree,重新渲染页面。

问题:对于多个相同TagName或相同runtimeType的DOM节点,如何复用?

答案:给Widget设置Key,保证唯一性。


4、Key的分类及使用

Key的定义

  • 官方定义如下:
    1
    2
    3
    Key是Widget、Element、SemanticsNode(语义节点)的标识符。
    只有当新的Widget的Key与当前Element中Widget的Key相同时,它才会被用来更新现有的Element。
    Key在具有相同父级的Element之间必须是唯一的。
  • Key的作用:diff算法的关键。

Key的分类

image.png

  • LocalKey
1
2
LocalKey直接继承自抽象类Key ,应用于拥有相同父级Element的Widget进行比较的场景。
例如:有一个多子Widget中需要对它的子Widget进行移动操作,此时可以使用LocalKey,提高Element复用率,提高性能。
  • GlobalKey
1
2
3
4
5
GlobalKey直接继承自抽象类Key,内部使用了一个静态常量Map来保存它对应的Element。
可以通过GlobalKey找到持有该GlobalKey的Widget、Element和State。
GlobalKey可以在多个页面或者层级复用。

注意:GlobalKey比较昂贵,需要谨慎使用。

GlobalKey的源码如下:

image.png

Key的使用


5、参考资料