美文网首页
Flutter - 从树开始了解Flutter

Flutter - 从树开始了解Flutter

作者: One1丨光 | 来源:发表于2019-04-11 18:44 被阅读0次

    本文不是一篇介绍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中。

    相关文章

      网友评论

          本文标题:Flutter - 从树开始了解Flutter

          本文链接:https://www.haomeiwen.com/subject/umapdqtx.html