Flutter三棵树构建过程

flutter的渲染机制基本就是靠Widget、Element、RenderObject三棵树去实现的,这篇博客就来讲讲这三棵树是怎么创建的。

首先我们来看看这三者到底是个啥:

  • Widget: 描述一个UI元素的配置数据,不可变,修改信息需要重新new

  • Element: 通过Widget配置实例化出来的对象,它是可变的

  • RenderObject: 真正的渲染对象

让我们用一个简单的demo来做讲解:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: HelloWorldPage(),
    );
  }
}

class HelloWorldPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text("Hello World", style: TextStyle(color: Colors.blue)),
    );
  }
}

上面的代码正在屏幕的中间显示了一个Hello World字符串。

1.png

runApp

在main函数里面只有一行runApp调用,追踪下去我们可以看到它主要做了三件事情:

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

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

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
    ...
    element = createElement();
    ...
    element!.mount(null, null);
    ...
  return element!;
}
  1. 创建RenderObjectToWidgetAdapter作为Widget树的根,将传入的Widget挂上去
  2. 调用RenderObjectToWidgetAdapter.createElement创建Element
  3. 调用Element.mount将它挂到Element树上,Element树的根节点的parent为null

Element.mount

Element的mount方法是三棵树创建流程的关键步骤,不同类型的Element mount的流程不太一样。

1.RenderObjectElement会创建RenderObject

如果Element是RenderObjectElement类型的,那么它对应的Widget一定是RenderObjectWidget类型的,这是它的构造函数决定的:

abstract class RenderObjectElement extends Element {
  RenderObjectElement(RenderObjectWidget widget) : super(widget);
  ...
}

它在mount的时候会调用RenderObjectWidget.createRenderObject创建RenderObject然后将它挂到RenderObject树上:

RenderObject get renderObject => _renderObject!;

RenderObject? _renderObject;

void mount(Element? parent, Object? newSlot) {
    ...
    _renderObject = widget.createRenderObject(this);
    ...
    attachRenderObject(newSlot);
    ...
}


void attachRenderObject(Object? newSlot) {
    ...
    // 插入RenderObject树
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    ...
}

这个_findAncestorRenderObjectElement方法比较魔性,找的是祖先RenderObjectElement,其实就是往parent一层层查找,直到找的RenderObjectElement:

RenderObjectElement? _findAncestorRenderObjectElement() {
    Element? ancestor = _parent;
    while (ancestor != null && ancestor is! RenderObjectElement)
      ancestor = ancestor._parent;
    return ancestor as RenderObjectElement?;
}

insertRenderObjectChild方法将创建的RenderObject插入成为祖先RenderObjectElement的RenderObject的子节点,这样就把创建的RenderObject挂到了RenderObject树上。

2.创建子Element并mount到Element树

处理完本节点的RenderObject之后,就会创建子Element将它的parent设置成自己,mount到Element树上。

Element都是通过Widget.createElement创建的,而Element会保存创建它的Widget。所以可以通过这个Widget去获取子Widget,然后用子Widget去创建子Element。

子Widget的获取有两种方式,如果是在Widget的构造函数传入的,那么直接可以拿到它,例如上面的RenderObjectToWidgetAdapter,然后用它去createElement创建子Element:

// 子widget是child参数传进去的
RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
)


void mount(Element? parent, Object? newSlot) {
    ...
    _rebuild();
    ...
}

void _rebuild() {
    ...
    // widget.child拿到构造函数传进去的子widget,即rootWidget
    _child = updateChild(_child, widget.child, _rootChildSlot);
    ...
}

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    ...
    newChild = inflateWidget(newWidget, newSlot);
    ...
}

Element inflateWidget(Widget newWidget, Object? newSlot) {
    ...
  // 创建子Element
    final Element newChild = newWidget.createElement();
    ...
  // 调用子Element的mount方法将它挂到Element树上,parent是第一个参数this
  newChild.mount(this, newSlot);
  ...
    return newChild;
}

像StatelessWidget这种子widget是build出来的,则在mount的时候会调用它的build方法创建子widget,然后用它去createElement创建子Element:

void mount(Element? parent, Object? newSlot) {
    ...
    _firstBuild();
    ...
}

void _firstBuild() {
    rebuild();
}

void rebuild() {
    ...
    performRebuild();
    ...
}

void performRebuild() {
    ...
    built = build();
    //updateChild在上面也有追踪这里就不列出来了,内部调用了built.createElement创建子Element并返回
    _child = updateChild(_child, built, slot);
    ...
}

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

最终得到的三棵树大概长下面的样子,由于没有分成所以看上去是链表而不是树,但是这不影响我们理解,一旦某些节点有多个child节点就是输了:

2.png

Element通过widget成员持有Widget,如果是RenderObjectElement还通过renderObject成员持有RenderObject,可以看出来Element是连接Widget和RenderObject的桥梁。三个树的构建也都是通过递归mount Element去实现的。

当RenderObject树创建出来之后,Flutter的引擎就能遍历它去执行绘制将画面渲染出来了。

mount流程解析

从上面的代码可以看得出来,mount是一个递归的过程,总结下来有下面几个步骤

  1. Element如果是RenderObjectElement则创建RenderObject,并从祖先找到上一个RenderObjectElement,然后调用祖先RenderObjectElement的RenderObject的insertRenderObjectChild方法插入创建的RenderObject
  2. 如果子widget需要build出来就调用build方法创建子widget,如果不需要直接在成员变量可以拿到子widget
  3. 调用子widget的createElement创建子Element
  4. 调用子Element的mount方法将子Element的parent设置成自己,然后子Element去到第1步

下面的动图展示了整个流程:

3.gif

或者可以下载PPT查看

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