Flutter渲染流程解析

Widget-Element-RenderObject

一. Flutter的渲染流程

1.1. Widget-Element-RenderObject关系

3棵tree的关系
3棵tree的关系

1.2. Widget是什么?

image-20200302153223929
image-20200302153223929

官方对Widget的说明:vue

  • Flutter的Widgets的灵感来自React,中心思想是构造你的UI使用这些Widgets。
  • Widget使用配置和状态,描述这个View(界面)应该长什么样子。
  • 当一个Widget发生改变时,Widget就会从新build它的描述,框架会和以前的描述进行对比,来决定使用最小的改变(minimal changes)在渲染树中,从一个状态到另外一个状态。

本身的理解:web

  • Widget就是一个个描述文件,这些描述文件在咱们进行状态改变时会不断的build。
  • 可是对于渲染对象来讲,只会使用最小的开销来更新渲染界面。

1.3. Element是什么?

image-20200302154618370
image-20200302154618370

官方对Element的描述:算法

  • Element是一个Widget的实例,在树中详细的位置。
  • Widget描述和配置子树的样子,而Element实际去配置在Element树中特定的位置。

1.4. RenderObject

image-20200302155014847
image-20200302155014847

官方对RenderObject的描述:数据结构

  • 渲染树上的一个对象
  • RenderObject层是渲染库的核心。

二. 对象的建立过程

咱们这里以Padding为例,Padding用来设置内边距app

2.1. Widget

Padding是一个Widget,而且继承自SingleChildRenderObjectWidget框架

继承关系以下:less

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
复制代码

咱们以前在建立Widget时,常用StatelessWidget和StatefulWidget,这种Widget只是将其余的Widget在build方法中组装起来,并非一个真正能够渲染的Widget(在以前的课程中其实有提到)。dom

在Padding的类中,咱们找不到任何和渲染相关的代码,这是由于Padding仅仅做为一个配置信息,这个配置信息会随着咱们设置的属性不一样,频繁的销毁和建立。编辑器

问题:频繁的销毁和建立会不会影响Flutter的性能呢?ide

  • 并不会,答案在个人另外一篇文章中;
  • https://mp.weixin.qq.com/s/J4XoXJHJSmn8VaMoz3BZJQ

那么真正的渲染相关的代码在哪里执行呢?

  • RenderObject

2.2. RenderObject

咱们来看Padding里面的代码,有一个很是重要的方法:

  • 这个方法实际上是来自RenderObjectWidget的类,在这个类中它是一个抽象方法;
  • 抽象方法是必须被子类实现的,可是它的子类SingleChildRenderObjectWidget也是一个抽象类,因此能够不实现父类的抽象方法
  • 可是Padding不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现
@override
RenderPadding createRenderObject(BuildContext context) {  return RenderPadding(  padding: padding,  textDirection: Directionality.of(context),  ); } 复制代码

上面的代码建立了什么呢?RenderPadding

RenderPadding的继承关系是什么呢?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
复制代码

咱们来具体查看一下RenderPadding的源代码:

  • 若是传入的_padding和原来保存的value同样,那么直接return;
  • 若是不一致,调用_markNeedResolution,而_markNeedResolution内部调用了markNeedsLayout;
  • 而markNeedsLayout的目的就是标记在下一帧绘制时,须要从新布局performLayout;
  • 若是咱们找的是Opacity,那么RenderOpacity是调用markNeedsPaint,RenderOpacity中是有一个paint方法的;
set padding(EdgeInsetsGeometry value) {
 assert(value != null);  assert(value.isNonNegative);  if (_padding == value)  return;  _padding = value;  _markNeedResolution();  } 复制代码

2.3. Element

咱们来思考一个问题:

  • 以前咱们写的大量的Widget在树结构中存在引用关系,可是Widget会被不断的销毁和重建,那么意味着这棵树很是不稳定;
  • 那么由谁来维系整个Flutter应用程序的树形结构的稳定呢?
  • 答案就是Element。
  • 官方的描述:Element是一个Widget的实例,在树中详细的位置。

Element何时建立?

在每一次建立Widget的时候,会建立一个对应的Element,而后将该元素插入树中。

  • Element保存着对Widget的引用;

在SingleChildRenderObjectWidget中,咱们能够找到以下代码:

  • 在Widget中,Element被建立,而且在建立时,将this(Widget)传入了;
  • Element就保存了对Widget的应用;
@override
 SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this); 复制代码

在建立完一个Element以后,Framework会调用mount方法来将Element插入到树中具体的位置:

mount方法
mount方法

在调用mount方法时,会同时使用Widget来建立RenderObject,而且保持对RenderObject的引用:

  • _renderObject = widget.createRenderObject(this);
@override
 void mount(Element parent, dynamic newSlot) {  super.mount(parent, newSlot);  _renderObject = widget.createRenderObject(this);  assert(() {  _debugUpdateRenderObjectOwner();  return true;  }());  assert(_slot == newSlot);  attachRenderObject(newSlot);  _dirty = false;  } 复制代码

可是,若是你去看相似于Text这种组合类的Widget,它也会执行mount方法,可是mount方法中并无调用createRenderObject这样的方法。

  • 咱们发现ComponentElement最主要的目的是挂载以后,调用_firstBuild方法
@override
 void mount(Element parent, dynamic newSlot) {  super.mount(parent, newSlot);  assert(_child == null);  assert(_active);  _firstBuild();  assert(_child != null);  }   void _firstBuild() {  rebuild();  } 复制代码

若是是一个StatefulWidget,则建立出来的是一个StatefulElement

咱们来看一下StatefulElement的构造器:

  • 调用widget的createState()
  • 因此StatefulElement对建立出来的State是有一个引用的
  • 而_state又对widget有一个引用
StatefulElement(StatefulWidget widget)
 : _state = widget.createState(),  ....省略代码  _state._widget = widget; 复制代码

而调用build的时候,本质上调用的是_state中的build方法:

Widget build() => state.build(this);
复制代码

2.4. build的context是什么

在StatelessElement中,咱们发现是将this传入,因此本质上BuildContext就是当前的Element

Widget build() => widget.build(this);
复制代码

咱们来看一下继承关系图:

  • Element是实现了BuildContext类(隐式接口)
abstract class Element extends DiagnosticableTree implements BuildContext 复制代码

在StatefulElement中,build方法也是相似,调用state的build方式时,传入的是this

Widget build() => state.build(this);
复制代码

2.5. 建立过程小结

Widget只是描述了配置信息:

  • 其中包含createElement方法用于建立Element
  • 也包含createRenderObject,可是不是本身在调用

Element是真正保存树结构的对象:

  • 建立出来后会由framework调用mount方法;
  • 在mount方法中会调用widget的createRenderObject对象;
  • 而且Element对widget和RenderObject都有引用;

RenderObject是真正渲染的对象:

  • 其中有 markNeedsLayout performLayout markNeedsPaint paint等方法

三. Widget的key

在咱们建立Widget的时候,老是会看到一个key的参数,它又是作什么的呢?

3.1. key的案例需求

咱们一块儿来作一个key的案例需求

key的案例需求
key的案例需求

home界面的基本代码:

class _HYHomePageState extends State<HYHomePage> {
 List<String> names = ["aaa", "bbb", "ccc"];   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("Test Key"),  ),  body: ListView(  children: names.map((name) {  return ListItemLess(name);  }).toList(),  ),   floatingActionButton: FloatingActionButton(  child: Icon(Icons.delete),  onPressed: () {  setState(() {  names.removeAt(0);  });  }  ),  );  } } 复制代码

注意:待会儿咱们会修改返回的ListItem为ListItemLess或者ListItemFul

3.2. StatelessWidget的实现

咱们先对ListItem使用一个StatelessWidget进行实现:

class ListItemLess extends StatelessWidget {
 final String name;  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));   ListItemLess(this.name);   @override  Widget build(BuildContext context) {  return Container(  height: 60,  child: Text(name),  color: randomColor,  );  } } 复制代码

它的实现效果是每删除一个,全部的颜色都会发现一次变化

  • 缘由很是简单,删除以后调用setState,会从新build,从新build出来的新的StatelessWidget会从新生成一个新的随机颜色
image-20200320151331285
image-20200320151331285

3.3. StatefulWidget的实现(没有key)

咱们对ListItem使用StatefulWidget来实现

class ListItemFul extends StatefulWidget {
 final String name;  ListItemFul(this.name): super();  @override  _ListItemFulState createState() => _ListItemFulState(); }  class _ListItemFulState extends State<ListItemFul> {  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));   @override  Widget build(BuildContext context) {  return Container(  height: 60,  child: Text(widget.name),  color: randomColor,  );  } } 复制代码

咱们发现一个很奇怪的现象,颜色不变化,可是数据向上移动了

  • 这是由于在删除第一条数据的时候,Widget对应的Element并无改变;
  • 而Element中对应的State引用也没有发生改变;
  • 在更新Widget的时候,Widget使用了没有改变的Element中的State;
image-20200320151747199
image-20200320151747199

3.4. StatefulWidget的实现(随机key)

咱们使用一个随机的key

ListItemFul的修改以下:

class ListItemFul extends StatefulWidget {
 final String name;  ListItemFul(this.name, {Key key}): super(key: key);  @override  _ListItemFulState createState() => _ListItemFulState(); } 复制代码

home界面代码修改以下:

body: ListView(
 children: names.map((name) {  return ListItemFul(name, key: ValueKey(Random().nextInt(10000)),);  }).toList(), ), 复制代码

这一次咱们发现,每次删除都会出现随机颜色的现象:

  • 这是由于修改了key以后,Element会强制刷新,那么对应的State也会从新建立
// Widget类中的代码
static bool canUpdate(Widget oldWidget, Widget newWidget) {  return oldWidget.runtimeType == newWidget.runtimeType  && oldWidget.key == newWidget.key; } 复制代码
image-20200320152321905
image-20200320152321905

3.5. StatefulWidget的实现(name为key)

此次,咱们将name做为key来看一下结果:

body: ListView(
 children: names.map((name) {  return ListItemFul(name, key: ValueKey(name));  }).toList(), ), 复制代码

咱们理想中的效果:

  • 由于这是在更新widget的过程当中根据key进行了diff算法
  • 在先后进行对比时,发现bbb对应的Element和ccc对应的Element会继续使用,那么就会删除以前aaa对应的Element,而不是直接删除最后一个Element
image-20200320152610235
image-20200320152610235

3.6. Key的分类

Key自己是一个抽象,不过它也有一个工厂构造器,建立出来一个ValueKey

直接子类主要有:LocalKey和GlobalKey

  • LocalKey,它应用于具备相同父Element的Widget进行比较,也是diff算法的核心所在;
  • GlobalKey,一般咱们会使用GlobalKey某个Widget对应的Widget或State或Element

3.6.1. LocalKey

LocalKey有三个子类

ValueKey:

  • ValueKey是当咱们以特定的值做为key时使用,好比一个字符串、数字等等

ObjectKey:

  • 若是两个学生,他们的名字同样,使用name做为他们的key就不合适了
  • 咱们能够建立出一个学生对象,使用对象来做为key

UniqueKey

  • 若是咱们要确保key的惟一性,可使用UniqueKey;
  • 好比咱们以前使用随机数来保证key的不一样,这里咱们就能够换成UniqueKey;

3.6.2. GlobalKey

GlobalKey能够帮助咱们访问某个Widget的信息,包括Widget或State或Element等对象

咱们来看下面的例子:我但愿能够在HYHomePage中直接访问HYHomeContent中的内容

class HYHomePage extends StatelessWidget {
 final GlobalKey<_HYHomeContentState> homeKey = GlobalKey();   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("列表测试"),  ),  body: HYHomeContent(key: homeKey),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.data_usage),  onPressed: () {  print("${homeKey.currentState.value}");  print("${homeKey.currentState.widget.name}");  print("${homeKey.currentContext}");  },  ),  );  } }  class HYHomeContent extends StatefulWidget {  final String name = "123";   HYHomeContent({Key key}): super(key: key);   @override  _HYHomeContentState createState() => _HYHomeContentState(); }  class _HYHomeContentState extends State<HYHomeContent> {  final String value = "abc";   @override  Widget build(BuildContext context) {  return Container();  } } 复制代码

备注:全部内容首发于公众号,以后除了Flutter也会更新其余技术文章,TypeScript、React、Node、uniapp、mpvue、数据结构与算法等等,也会更新一些本身的学习心得等,欢迎你们关注

公众号
公众号