Flutter渲染原理

2,179 阅读7分钟

总所周知,flutter中‘一切皆为Widget’,那widget是什么呢,这里有一个“总所周知”的答案就是:widget并不是真正的渲染对象。是的,事实上在Flutter中渲染是经历了从Widget到Element再到RenderObject的过程。

下面这张图是Flutter框架的整体结构组成:

image.png

Flutter Framework:纯Dart实现的SDK,它实现了一套基础课,用于处理动画、绘图和手势,并且基于绘图封装了一套UI组件库,然后根据Material和Cupertino两种视觉风格区分开来。这个纯Dart实现的SDK被封装为了一个叫做dart:ui的Dart库。我们在使用Flutter写App的时候,直接导入这个库即可使用组件等功能。

  • Framework的最底层叫做Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法。

  • 绘制库(Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等。

  • Animation是动画相关的类。

  • Gesture提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器。

  • 如果使用Flutter提供的控件进行开发,则需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接调用Render层,则需要使用RenderingFlutterBinding,而我们平时看到的大多数开发案例都是使用WidgetsFlutterBinding的。

Flutter Engine:纯 C++实现的 SDK,其中包括 Skia引擎、Dart运行时、文字排版引擎等。它是 Dart的一个运行时,它可以以 JIT 或者 AOT的模式运行 Dart代码。这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。

上面讲到,真正的渲染对象并非在 控件(Widget)层,而是在渲染(Rendering)层,Flutter中一共有三棵树,Widget树,Element树和RenderingObject树。

  • Widget:Widget里面存储了一个视图的配置信息,包括布局,属性等。Widget是不可变的。

  • Element:Element是实例化的 Widget 对象,当一个Widget首次被创建的时候,会通过 Widget 的 createElement() 方法,创建一个element,挂载到Element Tree遍历视图树。

image.png

在源代码的注释中可知:Widget和Element之间是一对多的关系,实际上渲染树是由Element实例的节点构成的树,而作为配置文件的Widget可能被复用到树的多个部分,对应产生多个Element对象。

  • RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息;在RenderObject的源码注释中写着 ‘An object in the render tree.’ 可以看出RenderObject才是实际的渲染对象。

大概总结出三者的关系是:配置文件Widget生成了Element,而后创建RenderObject关联到Element的内部renderObject对象上,最后Flutter通过RenderObject数据来布局和绘制。

每一个Element中都有着相对应的Widget和Renderobject的引用,每一个Element都具有一个唯一的key。当Widget树发生变化时,Flutter使用Element树来比较新的Widget树和旧的Widget树。通过对比 newWidget 与 oidWidget 的 runtimeType 和 key ,如果相等,就只需要修改RenderObject的配置,也就是重建widget即可;如果不相等,则需要重新创建Element,也就是重新实例化一个RenderObject。

@protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
**     **
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key key = newWidget?.key;
      if (key is GlobalKey) {
        key._debugReserveFor(this, newChild);
      }
      return true;
    }());

    return newChild;
  }

Element的生命周期

  • Framework 调用Widget.createElement 创建一个Element实例,记为element
  • Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
  • 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。
  • 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。
  • “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
  • 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

StatelessWidget

StatelessWidget继承自Widget类,重写了createElement()方法: @override StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。Statelesswidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI。

StatefulWidget

和StatelessWidget一样,StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()。

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => new StatefulElement(this);
    
  @protected
  State createState();
}

StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。

createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。

BuildContext

我们已经知道,StatelessWidget和StatefulWidget的build方法都会传一个BuildContext对象, 查看源码定义,发现其是一个抽象接口类 在源码中顺藤摸瓜,BuildContext就是widget对应的Element,所以我们可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。

image.png

但StatelessElement和StatefulElement本身并没有实现BuildContext接口,继续跟踪代码,发现它们间接继承自Element类,然后查看Element类定义,发现Element类果然实现了BuildContext接口:

1618290189165.jpg

如果没有widget层,单靠Element层是否可以搭建起一个可用的UI框架?之前说过widget树只是Element树的映射,我们完全可以直接通过Element来搭建一个UI框架.下面是使用Element层创建一个UI的例子

1618290432761.jpg