Flutter - 从树开始了解Flutter

本文不是一篇介绍Flutter入门知识,主要包括Flutter的视图管理和渲染机制。如果在阅读中有任何问题,麻烦Q442953298。欢迎喜欢Flutter的同学交流

Flutter中的树

Widget树

对Flutter有一定了解的同学,可能知道Flutter存在Everything’s a Widget的说法。在我们构建一个FlutterApp的时候,从main函数开始就调起了runApp函数。而runApp传入的参数就是一个Widget。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page')),
    );
  }
}

MyApp -> MaterialApp -> MyHomePage -> ...
组成了一个Widget树。
一个Widget的简单结构如下

// DiagnosticableTree 是一个调试类
// Wiget可以说是表现层的基类
abstract class Widget extends DiagnosticableTree {
    final Key key;
    @protected
    Element createElement();
}

而在StatelessWidget 和 State类中都实现了

Widget build(BuildContext context) {}

通过build方法父Widget可以访问到子节点的Widget

几个问题

  1. Stateless的Build方法由它本身实现,StatefulWidget的Build方法由State类实现。这种设计是出于对性能和数据-状态的响应等多方面的考虑。
    • 并不是所有的部件都需要关心数据/状态的变化,比如布局类、文本、图片等。他们对于状态的变化应该是无感知的。所以他们直接调用本身的Build方法来就可以轻松构造表现层。
    • StatefulWidget是有状态的Widget,这种说法其实是存在问题的。StatefulWidget继承自Widget,而Widget被@immutable标记,所以StatefulWidget本身也是不可变的。他之所以能够响应状态的变化,完全依赖于State类的实现。
    • 在Wiget树更新的时候,必然伴随着部件的创建和销毁.如果将状态交付给其他类管理,则可以在响应状态时轻松的改变Widget树而不影响数据层的维护。这样设计是数据层和表现层分离的良好体现。
  2. Widget类由@immutable标记,状态不可变。
    刚才我们说Widget的可变状态都是由State类进行管理,因此Widget自身的状态其实都是静态的,Widget从构造到被释放,自身的状态都应该保持不变。

Element树

Element - An instantiation of a [Widget] at a particular location in the tree.
一个在树中特定位置的Widget实例。
我们讨论Widget的时候提到Widget实现了一个方法:

@protected
  Element createElement();

在StatelessWidget和StatefulWidget重写了这个方法

abstract class StatelessWidget extends Widget {
    StatelessElement createElement() => StatelessElement(this);
}
abstract class StatefulWidget extends Widget {
  StatefulElement createElement() => StatefulElement(this);
}

StatelessElement构造方法只是简单的将Widget传给父类构造方法

StatelessElement(StatelessWidget widget) : super(widget);

StatefulElement的构造方法则不太相同

StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;
    _state._widget = widget;
  }
// 1. 调用传入的widget createState 创建了State对象
// 2. 赋值给自己的_state变量(可用get方法访问)
// 3. state对象分别持有了自身 和 传入的widget对象。

StatelessElement 和StatefulElement都继承自ComponentElement。ComponentElement声明了一个抽象方法

Widget build();

StatelessElement和StatefulElement都重写了build方法

// stateless实现
  @override
  Widget build() => widget.build(this);
// stateful实现
  @override
  Widget build() => state.build(this);

根据上面的实现我们可以简单的得出一个结论:

Widget的build都是由Element类的build方法触发。

Widget树的构建就和Element树就有了密不可分的关系,他们到底是怎么实现的。

runApp()以后发生了什么

  1. runApp(Widget app)

    void runApp(Widget app) {
      WidgetsFlutterBinding.ensureInitialized()
        ..attachRootWidget(app)
        ..scheduleWarmUpFrame();
    // WidgetsFlutterBinding.ensureInitialized() 创建了一个WidgetsFlutterBinding对象,他的主要任务就是协调Framework层和Application层的交互、进程、渲染等底层任务
    }
    
  2. attachRootWidget(app)

    // WidgetsFlutterBinding.dart 
     void attachRootWidget(Widget rootWidget) {
        _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
          container: renderView,
          debugShortDescription: '[root]',
          child: rootWidget
        ).attachToRenderTree(buildOwner, renderViewElement);
      }
    // 首先RenderObjectToWidgetAdapter 创建了 RenderObjectToWidgetAdapter对象
    // 接着attachToRenderTree 则生成了renderViewElement 
    // renderViewElement 通过RenderObjectToWidgetAdapter的createElement创建,它继承自RenderObjectToWidgetElement
    
    RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
    
    // RenderObjectToWidgetElement的widget字段持有了RenderObjectToWidgetAdapter对象
    
  3. WidgetsFlutterBinding 持有了Element树的根节点RenderObjectToWidgetElement;
    RenderObjectToWidgetElement持有了Widget树的根节点 RenderObjectToWidgetAdapter

  1. 关于RenderObjectToWidgetAdapter

    RenderObjectToWidgetAdapter({
    this.child,
    this.container,
    this.debugShortDescription
    }) : super(key: GlobalObjectKey(container));
    // 在第一第二块的attachRootWidget方法里我们将app当做child参数传给了RenderObjectToWidgetAdapter
    RenderObjectToWidgetAdapter<RenderBox>(
          container: renderView,
          debugShortDescription: '[root]',
          child: rootWidget
        )
    final Widget child;
    
    // 其中还有个参数需要被注意:
    container的参数传入的变量是renderView。
    renderView 在WidgetsFlutterBinding的构造方法里被创建,他S是后续我们会讲到的RenderObject树的根节点
    我们可以通过 WidgetsFlutterBinding.renderView或者WidgetsFlutterBinding.pipelineOwner.rootNode访问到它
    
  2. 关于RenderObjectToWidgetElement

    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
        if (element == null) {
          owner.lockState(() {
            element = createElement();
            element.assignOwner(owner);
          });
          owner.buildScope(element, () {
            element.mount(null, null);
          });
        } else {
          element._newWidget = this;
          element.markNeedsBuild();
        }
        return element;
    }
    // 在attachToRenderTree方法里会判断element参数是否为null如果为null则调用CreateElement,否则更新已存在的element的widget
    
  3. element.mount(null, null);
    上面我们已经提到

    WidgetsFlutterBinding -> element根节点 RenderObjectToWidgetElement -> widget根节点RenderObjectToWidgetAdapter

    element.mount 方法则开启了Element树和Widget树构建的大门

    // RenderObjectToWidgetElement mount 挂载
    void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _rebuild();
    }
    // 我们先不关心 super.mount在RenderObjectToWidgetElement的父类做了什么
    // _rebuild方法
    void _rebuild() {
        try {
          _child = updateChild(_child, widget.child, _rootChildSlot);
          // _child 作为参数传入第一次为null
          // widget.child 
          // widget是RenderObjectToWidgetAdapter
          // RenderObjectToWidgetAdapter.child 则是我们最初runApp传入的appWidget
        } catch (exception, stack) {
           // 抛出我们在页面上看见的红色背景黄字警告页面
        }
    }
    

    我们终于看到我们最初传入的AppWidget有了用武之地。

  4. updateChild方法 是构建和更新Widget树和Element树的灵魂

    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        if (newWidget == null) {
          if (child != null)
            deactivateChild(child);
          return null;
        }
        if (child != null) {
          if (child.widget == newWidget) {
            // 此处为更新slot代码
            return child;
          }
          if (Widget.canUpdate(child.widget, newWidget)) {
            // 此处为更新slot代码
            child.update(newWidget);
            return child;
          }
          deactivateChild(child);
          }
        return inflateWidget(newWidget, newSlot);
    }
    

    让我们用通俗的语言来描述这段代码的逻辑
    1.如果传入的newWidget为空而child不为空,则表明这是一次更新树的操作。newWidget为空表示我们在移除了一个Widget节点和它子Widget树,因此需要我们移除Element对应的child节点

    2.如果child不为空(表明是更新操作):

    2.1如果widget和新传入的newWidget是同一个对象,则说明这个节点没有任何变化返回child即可。(此处表明该节点可能是InheritWidget或者GlobalKey做了标记,此处后面的文章分析)

    2.2.如果widget与新传入的newWidget不是同一块内存,但是Widget.canUpdate(new,old) 的 runtimeType以及key都相同,则表明这个widget的深度和类型都没有发生改变,此时child这个Element对象可以继续拿来复用,只需要将旧的widget更新为新的即可。(此处在更新操作里最常见,比如setState后,build方法没有改变child节点的地方,则该节点下的所有widget都会走此处)

    2.3如果上面的情况都不满足,比如widget的节点虽然没有变化但是key发生了变化,再比如widget节点的类型由Text转换为Image类型,则这个节点的Element不能再复用,此时需要我们先移除Element节点

    3.当child为空,或者widget的key或类型发生了变化,就会走到这里,此时会调用inflateWidget方法,将新widget膨胀为一个Element。

    此处用膨胀其实是想说明Element的作用更加底层和重要

  5. inflateWidget魔法-一种递归创建Widget树和Element的方式

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
        assert(newWidget != null);
        final Key key = newWidget.key;
        if (key is GlobalKey) {
          final Element newChild = _retakeInactiveElement(key, newWidget);
          if (newChild != null) {
            return updatedChild;
          }
        }
        final Element newChild = newWidget.createElement();
        newChild.mount(this, newSlot);
        return newChild;
    }
    

    Globalkey此处的逻辑先不表,后续系列再主要介绍,当我们重新create了一个newChild的Element后,Element调用了自身的mount方法。
    此处有了一个大的设想,StatelessElement和StatefulElement的mout方法将会把他们build方法获得的widget继续updateChild-inflateWidget方式传递下去因此去做了验证

  6. ComponentElement
    StatelessElement 和 StatefulElement都继承自ComponentElement。在ComponentElement的mount方法里存在

    void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _firstBuild();
    }
    // _firstBuild()
    void _firstBuild() {
        rebuild();
    }
    // rebuild在element中实现,最后会调用performRebuild
    //performRebuild 在ComponentElement中实现
    void performRebuild() {
        Widget built;
        try {
          built = build();
         
        } catch (e, stack) {
          //error 处理
        } finally {
          _dirty = false;
        }
        try {
          _child = updateChild(_child, built, slot);
          assert(_child != null);
        } catch (e, stack) {
          //error 处理
        }
    }
    

    最终在performRebuild方法里知道了我们需要的东西。element会调用自身的build实现,获取当前widget节点下的build子节点。这个build方法创建的子节点会在调用updateChild方法的时候传入,因此形成了递归,一层一层的去构建或者说更新我们的element树,和Widget树。也就完整的完成了Element树和Widget树的调用。

SetState以后发生了什么

我们说刚才的流程是一个树创建的流程,那么树的更新是怎么开始的了?

  1. State. setState

    _element.markNeedsBuild();

  2. Element. didChangeDependencies

    markNeedsBuild();
    这两者都会引发markNeedsBuild()

void markNeedsBuild() {
    _dirty = true;
    owner.scheduleBuildFor(this);
}
// markNeedsBuild则会标记element被污染

scheduleBuildFor方法会将element add -> owner._dirtyElements里
并且标记element的_inDirtyList 为 true
然后发起一个等待Vsync信号渲染事件,当下一个Vsync信号来临时,会调用DrawFrame方法
此时buildScope方法重新被调用
你可能没有记住这个方法,你可以去上面查找一下attachWidgetTree 没错在第五节。
在attachToRenderTree方法里,如果element是新建的也会调用这个方法,这个方法的目的就是清空owner的_dirtyElements的element。
清空的方式就是调用他们的rebuild方法

_dirtyElements[index].rebuild();

因此回到了上面我们说的流程,更新和构建的区别,只在于构建是从顶点开始,并且是element树与Widget树向下交叉构建,而更新的流程会在Widget节点没有类型和key变化的前提下优先保留element,新建Widget节点的子树。更新的方式也是向下交叉的遍历。

RenderObject树

经过上面的介绍,可能存在很多复杂的逻辑绕来绕去,如果没有耐心查看源代码,大可以简单的认为,element树和Widget树的挂载是向下交叉完成的。
但我们了解Element树的意义似乎并没有表现出来,因为Element没有渲染合成相关的代码。所以可以认为Element并不是Layer合成的帮助者,这时候需要引入一个之前介绍树构建逻辑时忽略的对象RenderObject。

RenderObject的主要实现我们后续系列再表,它的主要作用就是将Widget的布局属性进行layout paint CompositingBits 等方式的计算,最后将渲染的计算结果提交图形引擎。
在Element的实现中

  RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element is RenderObjectElement)
        result = element.renderObject;
      else
        element.visitChildren(visit);
    }
    visit(this);
    return result;
  }

如果此对象是RenderObjectElement,则渲染对象是树中此位置处的对象。否则,这个getter将沿着树走下去,直到找到一个RenderObjectElement。
根节点的element 其实继承自RenderObjectElement
RnderObjectElement持有了一个_renderObeject对象,这个对象是由element.widget createRenderObject创建出来的。
在根节点RenderObjectToWidgetAdpater中返回的是RenderObjectWithChildMixin的类,这个类是在RenderObject的Mixin类,他实际返回的是WidgetFlutterBinding中的renderView 也即是pipelineOwner的rootNode。

不是每个Widget都存在CreateRenderObject方法。CreateRenderObject被声明在Widget的子类RenderObjectWidget中。

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

推荐阅读更多精彩内容