Widget是如何工作的

在前面我们介绍各种各样的Widget,相信大家对Wiget的使用都已经有了自己的认识,今天我们就从底层角度看下Widget是如何工作,是什么支撑起了Wiget这个系统。

其实,Widget并不是我们真正看到的视图,背后究竟是什么?其实Flutter Framework提供了三种视图树,即:Widget 、Element、 RenderObject,只不过,我们使用Flutter开发界面时,通常只和widget打交道。

image

Widget

Widget是用户界面的一部分,并且是不可变的(immutable)。Widget会被inflate到Element,并由Element管理底层渲染树。

Widget本身没有可变状态(所有的字段必须是final)。如果想要把可变状态与Widget关联起来,可以使用StatefulWidget,StatefulWidget通过使用StatefulWidget.createState方法创建State对象,并将之扩充到Element以及合并到树中。

Widget通过createElement方法使得子类创建Element

@immutable
abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({ this.key });
  ///Inflates this configuration to a concrete instance.
  @protected
  Element createElement();

Element

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

Element也可以理解为,Widget中额外的属性,可以用来存储Widget的状态和额外的值。

再来看看文档中对于Element的介绍:

An instantiation of a Widget at a particular location in the tree.

Element是在树中特定位置Widget的实例;

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

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

可以看到,Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject

由于Widget是不可变的,所以我们不能让Widget直接去跟RenderObject联系来进行渲染工作,因为如果这样我们每次改变一个Widget下层的Widget都需要重新构建,这大大增加了底层渲染的成本。

但是Element是可变的,我们可以借助于Element来和RenderObject沟通,只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

在第一次创建 Widget的时候,会对应创建一个Element, 然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget进行比较,并且相应地更新 Element。重要的是,Element 被不会重建,只是更新而已。

RenderObject

An object in the render tree

RenderObject渲染对象,从名字我们就可以很简答的知道它是真正负责渲染的,负责最终的layout和paint操作。

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

  void markNeedsLayout() {
      ...
  }
  
  void markNeedsPaint() {
      ...
  }
  
  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ...  
    if (sizedByParent) {
        performResize();
    }
    ...
    performLayout();
    ...
  }
  
  void performResize();
  
  void performLayout();
  
  void paint(PaintingContext context, Offset offset) { }
}

markNeedsLayout()标记这个RenderObject需要重新做布局。
markNeedsPaint标记这个RenderObject需要重绘。

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

深入理解Element

我们接下来通过StatelessElement来看下Element,在Widget中起了什么作用

首先我们看下基类Widget

@immutable
abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({ this.key });
  ///Inflates this configuration to a concrete instance.
  @protected
  Element createElement();

然后我们看下StatelessElement的源码

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);
  
  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);
}

可以看到,StatelessWidget通过createElement方法生成了一个StatelessElement。(注意:这里是StatelessWidget作为参数传递了进去)

然后通过build方法创建Widget并且需要传入一个BuildContext

现在我们来看StatelessElement它做了什么。

class StatelessElement extends ComponentElement {
  /// 通过 StatelessWidget来创建StatelessElement
  StatelessElement(StatelessWidget widget) : super(widget);

  ///通过父类获取Widget对象,StatelessWidget里createElement方法传入了StatelessWidget
  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  ///调用widget的build方法创建Widget,请注意这个传入初始化的值
  @override
  Widget build() => widget.build(this);

  ///更新Widget
  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}
  1. 首先通过 StatelessWidget来创建StatelessElement,当然这一步是在对应Widget中执行的。
  2. 通过父类获取Widget对象,StatelessWidget里createElement方法传入了StatelessWidget
  3. 调用widget的build方法创建Widget
  4. 最后通过根据需要执行update重新构建,并放入新的Widget

我们接着来看这个widget.build(this)方法。咦?这里怎么把StatelessElement给传进去了?不是要的是BuildContext吗?难道StatelessElement就是BuildContext的子类?

继续看源码。

class StatelessElement extends ComponentElement
...
abstract class ComponentElement extends Element
...

abstract class Element extends DiagnosticableTree implements BuildContext {
configuration.
[Widget.createElement].
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

  Element _parent;

好吧这就没错了,Element果然是BuildContext的子类那么这一切都说的通了。

RenderObjectWidget

接着往下面看,我们一直在调用build方法,我们在开发中也经常写包含Build的代码,到底怎么来绘制的

  @override
  Widget build(BuildContext context) {

    return  Container(
        alignment:Alignment.center,
        color: Colors.red,
        child: Text("我是test1的内容区域"),
    );
  }

首先传入了一个Container,由于它是一个布局所以它并不直接参与绘制,它往往只参与布局工作,绘制工作往往由相关的子Widget或者相关属性Widget来进行绘制。

以上面的例子为例,我们来看Container的build方法

@override
Widget build(BuildContext context) {
  Widget current = child;

  if (alignment != null)
    current = Align(alignment: alignment, child: current);
  
  if (effectivePadding != null)
    current = Padding(padding: effectivePadding, child: current);

  if (color != null)
    current = ColoredBox(color: color, child: current);

  if (decoration != null)
    current = DecoratedBox(decoration: decoration, child: current);
……

  return current;
}

实际上Container会根据相关的属性拆封成对应的组件来构建,由源码来看,这个过程是依次执行绘制的。

依我们的例子为例,我们首先根据alignment进行位置构建,然后根据color进行构建,最后才返回这Wdiget。

可以看到,alignment和color对child分别执行了一直构建操作,最后交给了Text来进行处理。

说了这么多到底怎么来绘制啊,接着外下看

class Align extends SingleChildRenderObjectWidget {}
class Padding extends SingleChildRenderObjectWidget {}
class ColoredBox extends SingleChildRenderObjectWidget {}
class ColoredBox extends SingleChildRenderObjectWidget {}

Text的build方法中返回了一个RichText,所以Text本身也是RichText,这里不再贴这部分源码了。


class RichText extends MultiChildRenderObjectWidget {}

这两个类有一个共同的子类那就是RenderObjectWidget。

继续来看RenderObjectWidget方法

abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();

  @protected
  RenderObject createRenderObject(BuildContext context);

  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}

RenderObjectWidget是一个抽象类,MultiChildRenderObjectWidget和SingleChildRenderObjectWidget都是它的子类,这个类中同时拥有创建 Element、RenderObject,以及更新 RenderObject 的方法。

但实际上,RenderObjectWidget 本身并不负责这些对象的创建与更新

对于 Element 的创建,Flutter 会在遍历 Widget 树,调用 createElement 去同步 Widget 自身配置,从而生成对应节点的 Element 对象。而对于 RenderObject 的创建与更新,其实是在 RenderObjectElement 类中完成的。

同样的来看RenderObjectElement

abstract class RenderObjectElement extends Element {
  RenderObjectElement(RenderObjectWidget widget) 
  @override
  RenderObjectWidget get widget => super.widget as 
  @override
  RenderObject get renderObject => _renderObject;
  @override   
  
  void mount(Element parent, dynamic newSlot) {                 super.mount(parent, newSlot); 
  _renderObject = widget.createRenderObject(this);     attachRenderObject(newSlot);     
  _dirty = false;  
  }
  
  @override void update(covariant RenderObjectWidget newWidget) { 
    super.update(newWidget); 
    widget.updateRenderObject(this, renderObject);
  _dirty = false; }
}


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

继续来看StatelessElement的update方法

void update(covariant Widget newWidget) {
 
  assert(_debugLifecycleState == _ElementLifecycle.active
      && widget != null
      && newWidget != null
      && newWidget != widget
      && depth != null
      && _active
      && Widget.canUpdate(widget, newWidget));
  assert(() {
    _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
    _debugForgottenChildrenWithGlobalKey.clear();
    return true;
  }());
  _widget = newWidget;
}

判断新的widget是否与老的widget相同,如果不是同一个Widget就执行,遍历View树并移除子Widget,最后在原来的位置放上新的Widget。

对于StatelessElement它仅在第一个初始化和hot reload时会被调用,并且只会在应用active时有效。

到这里Flutter中Widget的创建基本流程就完成了,下篇我们来看下StatfulWidget的基本流程。

小结

  • 在Flutter中Widget是不可变的并不负责界面的绘制。

  • Element 则是 Widget 的一个实例化对象,我们经常用到BuildContext就是Element的子类。

  • RenderObject负责界面的布局和绘制。

  • RenderObjectWidget(RenderObjectElement)是这样一个Widget,它通过mount方法来创建一个RenderObject.

关注Flutter开发者获取更多文章


image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342