Widget、Element和RenderObject之间的转化

开篇

image

Flutter中页面的渲染渲染离不开三个重要的元素:Widget、Element、RenderObject。是一个从Widget到Element再到RenderObject的过程。而具体到源码中,这个转换的工作时如何实现的呢,今天我们就来跟着源码简略分析一下。为了更流程的分析它们三者之间的关系,我们先根据Framework层源码的注释了解一下它们,然后再从Flutter项目入口进行分析。

Widget、Element和RenderObject

根据源码注释我么可以大致知道:

  1. Widget是对UI的一种廉价的、不可变的,描述(如何配置子树)Element的配置,并可以生成Element
  2. Widget本身没有状态,所有属性都是final,Element持有WidgetRenderObject的实例
  3. 给定Widget可不断加入到树中不同的位置,并生成Element作为它在树中的实例,它们是一对多的关系
  4. RenderObject是实际的渲染对象,Element就可以理解为将变化的Widget转化为较为稳定的RenderObject

Flutter工程的入口

新建一个Flutter项目后,我们会看到其工程目录如下

image

其中,main.dart文件中有main函数作为程序的入口:

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

其中MyApp()就是一个Widget,我们忽略其他代码,只关心它在代码中的走向,发现它最终作为一个child用于RenderObjectToWidgetAdapter的初始化,并且通过attachToRenderTree方法生成了一个Element

  void attachRootWidget(Widget rootWidget) {
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
  }  

继续看它的attachToRenderTree方法:

 RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null);
      });
      // This is most likely the first time the framework is ready to produce
      // a frame. Ensure that we are asked for one.
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

可以发现:在我们创建的“根”Widget之前,会有一个RenderObjectToWidgetAdapter作为真正的根Widget,而与之对应的是一个RenderObjectToWidgetElement作为根Element。通过调用createElement方法初始化与之对应的Element。该方法在抽象类Widget中被定义,可被子类重写。
而观察Element的初始化方式以及定义:

Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

不难发现:Element初始化时接受了当前的Widget并作为私有变量持有。那么我们的Widget,MyApp是在何时转换成Element的呢?

自上而下的调用创建Element

继续追踪attachToRenderTree方法源码,我们发现在Element被初始化后,会调用element.mount(null, null)方法(只保留关键代码),该代码在framework.dart文件中的Element类中。

void mount(Element parent, dynamic newSlot) {
    assert(parent == null);
    super.mount(parent, newSlot);
    _rebuild();
  }
void _rebuild() {
    try {
      //_child是其子Element,也就是`MyApp`将要生成的Element,此时进行初始化
      //widget.child是我们的`MyApp`
      _child = updateChild(_child, widget.child, _rootChildSlot);
      assert(_child != null);
    } catch (exception, stack) {
      ...
    }
  }
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
       //如果newWidget为空,说明没有子树,返回null
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    //此时_child为null
    if (child != null) {
      ...
    } else {
      //初始化
      newChild = inflateWidget(newWidget, newSlot);
    }
    return newChild;
  }
  
Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        assert(newChild._parent == null);
        assert(() {
          _debugCheckForCycles(newChild);
          return true;
        }());
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      }
    }
    //调用`MyApp`的`createElement`方法进行初始化
    final Element newChild = newWidget.createElement();
    assert(() {
      _debugCheckForCycles(newChild);
      return true;
    }());
    //执行monut方法,继续下一个Widget的转换
    newChild.mount(this, newSlot);
    assert(newChild._debugLifecycleState == _ElementLifecycle.active);
    return newChild;
  }

可以发现,当我们的程序启动时,runApp会创建一个根Widget并调用其createElement方法生成element。并执行element的mount方法。之后,element会读取并判断自己所持有的Widget对象是否有子Widget,就是上面代码中widget.child。当发现child不为空,就会继续执行createElement()方法,并执行mount()方法进入下一次子树。依次类推直至遍历完所有子树。

依旧是自上而下的调用却有点不一样

在追踪上述代码时,我们并没有直接发现类似createElementcreateRenderObject()方法被调用,我们观察RenderObjectToWidgetElement的继承关系:

image

最终可在RenderObjectElementmount()方法里看到这样的代码:

@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    ....
    _renderObject = widget.createRenderObject(this);
    ....
  }

在这里,我们发现了初始化RenderObject的地方。可以发现,RenderObjectWidget实在Element里被初始化,并且和Widget一样被Element所持有。并且,采取和Element一样,自上而下的不断遍历整个树进行初始化。
但是有一点我们需要注意:
createElement()方法在所有widget的基类Widget中被定义了。但是createRenderObject()方法是在Widget的子类RenderObjectWidget中被定义的,于此同时,createRenderObject()方法也只会在RenderObjectElement中被调用。
这说明:每个Widget都会生成一个Element,但并不是每个Element都会创建一个RenderObject,只有RenderObjectWidget类型的子类的Widget创建的继承自RenderObjectElement类型的element才会创建RenderObject。也就是说,widget树、element树和RenderObject树并不是一是一对应的。

image

总结

本文只是Flutter UI相关的冰山一角,并没有深入了解Flutter的绘制原理。旨在通过源码了解Widget、Element和RenderObject直接的关系,理清他们之间的逻辑。

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