Flutter框架分析(三)-- Widget,Element和RenderObject

前言

前面两篇Flutter框架分析的文章介绍了渲染流水线,window和框架的初始化。这篇文章继续来理一下对Flutter app开发者来说比较重要的WidgetElementRenderObject体系。Flutter的理念是一切都是Widget(Everythin is Widget)。开发者在开发Flutter app的时候主要都是在写很多Widget。那么这三者之间是什么关系?它们是怎么工作的呢?让我们来一探究竟。

概览

这块的内容比较多且有些复杂,为了不让大家迷失在源码的海洋里,我们还是举个例子先简单了解一下这个体系。

void main() {
  runApp(MyWidget());
}

class MyWidget extends StatelessWidget {
  final String _message = "Flutter框架分析";
  @override
  Widget build(BuildContext context) => ErrorWidget(_message);
}

这个例子的利用Flutter自带的ErrorWidget显示我们自定义的一句话:“Flutter框架分析”。没错,这个ErrorWidget就是当你的代码出bug的时候显示在屏幕上的可怕的红底黄字信息。放张截屏大家感受一下。

image

这里使用它是因为它是最简单,层级最少的一个Widget。以方便我们理解Flutter框架,避免被MaterialApp那深不可测的element tree和render tree劝退。

运行上述例子以后再打开Flutter Inspector看一下:

element tree

从上图可见就三个层级 root->MyWidget->ErrorWidget。这看起来是个widget tree。这里的root对应的是上篇文章里说的RenderObjectToWidgetAdapter。但这实际上是这样的一个element tree:RenderObjectToWidgetElement->StatelessElement->LeafRenderObjectElement。还记得我们上篇文章里说的,RenderObjectToWidgetElement是element tree的根节点。看看图中上方红框,这个根节点是持有render tree的根节点RenderView的。它的子节点就是我们自己写的MyWidget对应的StatelessElement。而这个element是不持有RenderObject的。只有最下面的ErrorWidget对应的LeafRenderObjectElement才持有第二个RenderObject。所以 render tree是只有两层的: RenderView->RenderErrorBox。以上所说用图来表示就是这样的:

widget element renderobject

图中绿色连接线表示的是element tree的层级关系。黄色的连接线表示render tree的层级关系。

从上面这个例子可以看出来,Widget是用来描述对应的Element的描述或配置。Element组成了element tree,Element的主要功能就是维护这棵树,节点的增加,删除,更新,树的遍历都在这里完成。Element都是从Widget中生成的。每个Widget都会对应一个Element。但是并非每个Widget/Element会对应一个RenderObject。只有这个Widget继承自RenderObjectWidget的时候才会有对应的RenderObject

总的来说就是以下几点:

  • Widget是对Element的配置或描述。Flutter app开发者主要的工作都是在和Widget打交道。我们不需要关心树的维护更新,只需要专注于对Widget状态的维护就可以了,大大减轻了开发者的负担。
  • Element负责维护element tree。Element不会去管具体的颜色,字体大小,显示内容等等这些UI的配置或描述,也不会去管布局,绘制这些事,它只管自己的那棵树。Element的主要工作都处于渲染流水线的构建(build)阶段。
  • RenderObject负责具体布局,绘制这些事情。也就是渲染流水线的布局(layout)和 绘制(paint)阶段。

接下来我们就结合源码,来分析一下WidgetElementRenderObject

Widget

基类Widget很简单

@immutable
abstract class Widget extends DiagnosticableTree {

  const Widget({ this.key });
  ...
  @protected
  Element createElement();
  ...
}

方法createElement()负责实例化对应的Element。由其子类实现。接下来看下几个比较重要的子类:

StatelessWidget

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对Flutter开发者来讲再熟悉不过了。它的createElement方法返回的是一个StatelessElement实例。

StatelessWidget没有生成RenderObject的方法。所以StatelessWidget只是个中间层,它需要实现build方法来返回子Widget

StatefulWidget

abstract class StatefulWidget extends Widget {
  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();
}

StatefulWidget对Flutter开发者来讲非常熟悉了。createElement方法返回的是一个StatefulElement实例。方法createState()构建对应于这个StatefulWidgetState

StatefulWidget没有生成RenderObject的方法。所以StatefulWidget也只是个中间层,它需要对应的State实现build方法来返回子Widget

State

说到StatefulWidget就不能不说说State

abstract class State<T extends StatefulWidget> extends Diagnosticable {
  T get widget => _widget;
  T _widget;
  
  BuildContext get context => _element;
  StatefulElement _element;

  bool get mounted => _element != null;

  void initState() { }

  void didUpdateWidget(covariant T oldWidget) { }

  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

  void deactivate() { }
  
  void dispose() { }

  Widget build(BuildContext context);

  void didChangeDependencies() { }
}

从源码可见,State持有对应的WidgetElement。注意这一句BuildContext get context => _element;。我们在调用build时候的入参BuildContex其实返回的就是Element

mounted,用来判断这个State是不是关联到element tree中的某个Element。如果当前State不是在mounted == true的状态,你去调用setState()是会crash的。

函数initState()用来初始化State

函数didUpdateWidget(covariant T oldWidget)在这个State被换了个新的Widget以后被调用到。是的,State对应的Widget实例只要是相同类型的是可以被换来换去的。

函数setState()我们很熟悉了。这个函数只是简单执行传入的回调然后调用_element.markNeedsBuild()。你看,如果此时_element为空的时候会不会出问题?所以建议大家在调用setState()之前用mounted判断一下。另外要注意的一点是,这个函数也是触发渲染流水线的一个点。后续我会在另外的文章里从这个点出发,给大家说说渲染流水线如何在WidgetElementRenderObject架构下运行。

函数deactivate()State对应的Element被从树中移除后调用,这个移除可能是暂时移除。

函数dispose()State对应的Element被从树中移除后调用,这个移除是永久移除。

函数build(BuildContext context),大家很熟悉了,不多说了。

函数didChangeDependencies()State的依赖发生变化的时候被调用,具体什么样的依赖后文再说。

StatefullWidgetState对Flutter app开发者来说可能会是打交道最多的。有些细节还需要结合Element做深入的理解。

InheritedWidget

InheritedWidget既不是StatefullWidget也不是StatelessWidget。它是用来向下传递数据的。在InheritedWidget之下的子节点都可以通过调用BuildContext.inheritFromWidgetOfExactType()来获取这个InheritedWidget。它的createElement()函数返回的是一个InheritedElement

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

RenderObjectWidget

RenderObjectWidget用来配置RenderObject。其createElement()函数返回RenderObjectElement。由其子类实现。相对于上面说的其他Widget。这里多了一个createRenderObject()方法。用来实例化RenderObject

abstract class RenderObjectWidget extends Widget {

  const RenderObjectWidget({ Key key }) : super(key: key);

  @override
  RenderObjectElement createElement();

  @protected
  RenderObject createRenderObject(BuildContext context);

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

  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}

RenderObjectWidget只是个配置,当配置发生变化需要应用到现有的RenderObject上的时候,Flutter框架会调用updateRenderObject()来把新的配置设置给相应的RenderObject

RenderObjectWidget有三个比较重要的子类:

  • LeafRenderObjectWidget这个Widget配置的节点处于树的最底层,它是没有孩子的。对应LeafRenderObjectElement
  • SingleChildRenderObjectWidget,只含有一个孩子。对应SingleChildRenderObjectElement
  • MultiChildRenderObjectWidget,有多个孩子。对应MultiChildRenderObjectElement

Element

Element构成了element tree。这个类主要在做的事情就是维护这棵树。
从上面对Widget的分析我们可以看出,好像每个特别的Widget都会有一个对应的Element。特别是对于RenderObjectWidget。如果我有一个XXXRenderObjectWidget,它的createElement()通常会返回一个XXXRenderObjectElement。为简单起见。我们的分析就仅限于比较基础的一些Element
首先来看一下基类Element

abstract class Element extends DiagnosticableTree implements BuildContext {
    Element _parent;
    Widget _widget;
    BuildOwner _owner;
    dynamic _slot;
    
    void visitChildren(ElementVisitor visitor) { }
    
    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        
    }
    
    void mount(Element parent, dynamic newSlot) {
        
    }
    
    void unmount() {
         
    }
    
    void update(covariant Widget newWidget) {
        
    }
    
    @protected
    Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
      final Element newChild = newWidget.createElement();
      newChild.mount(this, newSlot);
      return newChild;
    }
  
    void markNeedsBuild() {
      if (dirty)
        return;
      _dirty = true;
      owner.scheduleBuildFor(this);
    }
    
    void rebuild() {
      if (!_active || !_dirty)
        return;
      performRebuild();
    }
  
    @protected
    void performRebuild();
}

Element持有当前的Widget,一个BuildOwner。这个BuildOwner是之前在WidgetsBinding里实例化的。Element是树结构,它会持有父节点_parent_slot由父Element设置,目的是告诉当前Element在父节点的什么位置。由于Element基类不知道子类会如何管理孩子节点。所以函数visitChildren()由子类实现以遍历孩子节点。

函数updateChild()比较重要,用来更新一个孩子节点。更新有四种情况:

  • Widget为空,老Widget也为空。则啥也不做。
  • Widget为空,老Widget不为空。这个Element被移除。
  • Widget不为空,老Widget为空。则调用inflateWidget()以这个Wiget为配置实例化一个Element
  • Widget不为空,老Widget不为空。调用update()函数更新子Elementupdate()函数由子类实现。

Element被实例化以后会调用mount()来把自己加入element tree。要移除的时候会调用unmount()

函数markNeedsBuild()用来标记Element为“脏”(dirty)状态。表明渲染下一帧的时候这个Element需要被重建。

函数rebuild()在渲染流水线的构建(build)阶段被调用。具体的重建在函数performRebuild()中,由Element子类实现。

Widget有一些比较重要的子类,对应的Element也有一些比较重要的子类。

ComponentElement

ComponentElement表示当前这个Element是用来组合其他Element的。

abstract class ComponentElement extends Element {
  ComponentElement(Widget widget) : super(widget);

  Element _child;

  @override
  void performRebuild() {
    Widget built;
    built = build();
    _child = updateChild(_child, built, slot);
  }

  Widget build();
}

ComponentElement继承自Element。是个抽象类。_child是其孩子。在函数performRebuild()中会调用build()来实例化一个Widgetbuild()函数由其子类实现。

StatelessElement

StatelessElement对应的Widget是我们熟悉的StatelessWidget

class StatelessElement extends ComponentElement {

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    _dirty = true;
    rebuild();
  }
}

build()函数直接调用的就是StatelessWidget.build()。现在你知道你写在StatelessWidget里的build()是在哪里被调用的了吧。而且你看,build()函数的入参是this。我们都知道这个函数的入参应该是BuildContext类型的。这个入参其实就是这个StatelessElement

StatefulElement

StatefulElement对应的Widget是我们熟悉的StatefulWidget

class StatefulElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;
    _state._widget = widget;
  }

  @override
  Widget build() => state.build(this);
  
   @override
  void _firstBuild() {
    final dynamic debugCheckForReturnedFuture = _state.initState() 
    _state.didChangeDependencies();
    super._firstBuild();
  }

  @override
  void deactivate() {
    _state.deactivate();
    super.deactivate();
  }

  @override
  void unmount() {
    super.unmount();
    _state.dispose();
    _state._element = null;
    _state = null;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _state.didChangeDependencies();
  }
}

StatefulElement构造的时候会调用对应StatefulWidgetcreateState()函数。也就是说State是在实例化StatefulElement的时候被实例化的。并且State实例会被这个StatefulElement实例持有。从这里也可以看出为什么StatefulWidget的状态要由单独的State管理,每次刷新的时候可能会有一个新的StatefulWidget被创建,但是State实例是不变的。

build()函数调用的是我们熟悉的State.build(this),现在你也知道了Statebuild()函数是在哪里被调用的了吧。而且你看,build()函数的入参是this。我们都知道这个函数的入参应该是BuildContext类型的。这个入参其实就是这个StatefulElement

我们都知道State有状态,当状态改变时对应的回调函数会被调用。这些回调函数其实都是在StatefulElement里被调用的。

在函数_firstBuild()里会调用State.initState()State.didChangeDependencies()

在函数deactivate()里会调用State.deactivate()

在函数unmount()里会调用State.dispose()

在函数didChangeDependencies()里会调用State.didChangeDependencies()

InheritedElement

InheritedElement对应的WidgetInheritedWidget。其内部实现主要是在维护对其有依赖的子ElementMap,以及在需要的时候调用子Element对应的didChangeDependencies()回调,这里就不贴代码了,大家感兴趣的话可以自己去看一下源码。

RenderObjectElement

RenderObjectElement对应的WidgetRenderObjectWidget

abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;
  
  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
  
  @override
  void unmount() {
    super.unmount();
    widget.didUnmountRenderObject(renderObject);
  }
  
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  
  @override
  void performRebuild() {
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  
  @protected
  void insertChildRenderObject(covariant RenderObject child, covariant dynamic slot);

  @protected
  void moveChildRenderObject(covariant RenderObject child, covariant dynamic slot);

  @protected
  void removeChildRenderObject(covariant RenderObject child);

}

函数mount()被调用的时候会调用RenderObjectWidget.createRenderObject()来实例化RenderObject

函数update()performRebuild()被调用的时候会调用RenderObjectWidget.updateRenderObject()

函数unmount()被调用的时候会调用RenderObjectWidget.didUnmountRenderObject()

RenderObject

RenderObject负责渲染流水线布局(layout)阶段和绘制(paint)阶段的工作。同时也维护render tree。对render tree的维护方法是来自基类AbstractNode。这里我们主要关注和渲染流水线相关的一些方法。

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框架会调度一帧,在下一个Vsync信号到来之后才真正做布局和绘制。

真正的布局在函数layout()中进行。这个函数会做一次判断,如果sizedByParenttrue。则会调用performResize()。表明这个RenderObject的尺寸仅由其父节点决定。然后会调用performLayout()做布局。performResize()performLayout()都需要RenderObject的子类去实现。`

总结

WidgetElementRenderObject体系是Flutter框架的核心。其中Element需要好好理解。Flutter的渲染流水线中的构建(build)阶段主要就是在维护更新element tree里面的Element节点。只有理解了Element和element tree,才是真正掌握了Flutter框架。这篇文章里只是一些静态的说明。下篇文章我会尝试从渲染流水线动态运行的角度分析一下Flutter框架是怎么运行的。

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

推荐阅读更多精彩内容

  • 原文在此,此处只为学习 Widget与ElementWidget主要接口Stateless WidgetState...
    lltree阅读 4,499评论 0 1
  • 目录 一、Flutter 为何使用Dart开发语言二、Flutter的UI系统1.特点2.架构简介2.1 Flut...
    十拿九稳啦阅读 3,626评论 3 28
  • 国庆后面两天在家学习整理了一波flutter,基本把能撸过能看到的代码都过了一遍,此文篇幅较长,建议保存(star...
    Nealyang阅读 4,337评论 1 17
  • (一) 走出芙蓉广场地铁站五号出口,阵阵寒风迎面袭来,我不禁耸肩,手拽了拽身上的羽绒服。 抬头望去,中天广场写字楼...
    寂寞捕手阅读 744评论 19 14
  • “家一不小心就变成一个没有温暖、只有压迫的地方。外面的世界固然荒凉。但是家却可以更寒冷。” 1 天刚蒙蒙亮,我就醒...
    孚苼阅读 576评论 2 5