Flutter运行过程(二):Flutter如何渲染第一帧

175 阅读9分钟

本系列将从Flutter框架runApp()运行开始,结合框架源码,分析flutter UI渲染、更新机制,布局、绘制过程,以及解析flutter主要的生命周期过程。认真读完本系列,读者一定会对Flutter运行过程了如指掌、胸有成竹。

本系列将有小量源码出没,建议读者打开编译器,配合框架源码食用,效果更佳。

开始的开始

前文提到,Flutter通过注册VSync信号监听,来更新”脏“元素,那Flutter是如何显示第一帧的呢?也需要等待VSync信号回调吗?

本文主要介绍Flutter渲染第一帧时所做的工作。

Flutter App的入口参数是runApp()

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Counter Demo",
      ...
      theme: ThemeData(primarySwatch: Colors.blue),
    );
  }
}

Flutter不仅需要在runApp()中完成对框架的初始化,同时还需要将app的第一帧画面渲染显示出来。

接着上文的内容,我们在上文提到,在ensureInitialized()中,主要进行framework的初始化工作,包括BuildOwner、RenderView等重要对象的初始化,以及注册绘制及各种回调函数等等。

那紧跟后面联调的scheduleAttachRootWidget()和scheduleWarmUpFrame()负责什么内容呢?

scheduleAttachRootWidget()

先看看scheduleAttachRootWidget()

// WidgetsBinding类
@protected
void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

attachRootWidget()的实现如下:

// WidgetsBinding类
void attachRootWidget(Widget rootWidget) {
  final bool isBootstrapFrame = renderViewElement == null;
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
  }
}

首先,调用RenderObjectToWidgetAdapter()构造方法,并将renderView,以及rootWidget传入。

  1. renderView:在ensureInitialized()阶段完成初始化,是一个RenderObject对象,也是整个页面RenderObject树的根
  2. rootWidget:是我们需要构建的widget,例子中是一个StatelessWidget对象。

RenderObjectToWidgetAdapter类结构如下,是一个RenderObjectWidget对象:

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {}

创建完Widget对象后,调用attachToRenderTree(),把buildOwner,renderViewElement对象传入,其中

  1. buildOwner:也是初始化阶段赋值的对象,它负责整个页面的组件管理工作,内部维护了一个dirtyElements列表,更新时会遍历这个列表,进行rebuild。
  2. renderViewElement:它是一个RenderObjectElement对象,值取自_renderViewElement,此时值为null
// RenderObjectToWidgetAdapter类
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);
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element!;
}
一、element为null

调用createElement(),返回一个RenderObjectElement对象,创建element时把widget作为参数,传给了element构造方法,此时element就持有了widget对象。

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
}

接着调用element.assignOwner(owner);,将这个element与页面的buildOwner对象绑定,此时就与BuildOwner对象建立了联系,当该element需要更新时,会被加入到BuildOwner对象中的dirtElements列表中。

随后调用element.mount(),参数传了两个null,因为是根element,所以自然不会有parent,而slot是个什么对象呢?它是描述子element在父element树中具体的配置信息,可以理解为子element将在父element中的位置,slot也为null。

为什么需要slot?我们知道element以widget作为配置信息来创建,一个widget配置信息可能会用于创建多个element,但这多个element最终可能会被挂在同一棵树上,widget是不负责决定生成的element最终会被挂在树上的哪个地方,所以需要这个slot对象区分各个element。

class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement {
  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    _rebuild();
  }
}

内部做了那些事?

  1. 调用super.mount(),其内部又通过super.mount(),一步步调用到了Element.mount(),这个方法一共做了那些事?

    // Element.mount()
    _parent = parent;
    _slot = newSlot;
    _lifecycleState = _ElementLifecycle.active;
    _depth = _parent != null ? _parent!.depth + 1 : 1;
    if (parent != null) {
      _owner = parent.owner;
    }
    final Key? key = widget.key;
    if (key is GlobalKey) {
      owner!._registerGlobalKey(key, this);
    }
    _updateInheritance();
    
    // RenderObjectElement.mount()
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
    
    
    Element.mount()

    Element.mount()中实际上只是做一些赋值操作,也能理解,Element属于最底层的对象,里面正常都是进行各种Element的通用操作,比如配置赋值,如果该widget设置了global key,则会在这里进行注册

    GlobalKey是有关Element复用的,如果widget提供了key,那么会把该element存在BuildOwner的复用列表中,后面有需要再根据key,取出对应的element达到复用效果,这里简单提一下,咱先不理

    RenderObjectElement.mount()

    RenderObjectElement.mount()中,调用widget.createRenderObject(),并把Element对象传入,这个widget是我们前面创建的RenderObjectToWidgetAdapter对象,看看这个类的实现。

    // RenderObjectToWidgetAdapter类
    @override
    RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
    

    这个container其实就是我们一开始构建Widget对象时传入的renderView(RenderObject),此时用于窗口绘制的三个顶层对象Widget、Element、RenderObject已经集齐了

    接着调用attachRenderObject(),并把该Element的位置参数slot传入,将创建的RenderObject对象挂载到父element的RenderObject树中,但因为该Element本来就是Element树中的根Element,所以实际上在渲染第一帧时attachRenderObject()里并没有做什么操作。

    最后设置_dirty = false,mount()操作完成。

    总结一下,mount主要完成以下工作:一是创建RenderObject对象,二是如果有父Element,就将RenderObject挂载到父Element的RenderObject树中

  2. super.mount()执行完毕,此时根Widget、Element、RenderObject都准备好了,调用_rebuild()操作。

    // RenderObjectToWidgetElement类,继承自RootRenderObjectElement
    void _rebuild() {
      try {
        _child = updateChild(_child, widget.child, _rootChildSlot);
      } catch (exception, stack) {
        final FlutterErrorDetails details = FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'widgets library',
          context: ErrorDescription('attaching to the render tree'),
        );
        FlutterError.reportError(details);
        final Widget error = ErrorWidget.builder(details);
        _child = updateChild(null, error, _rootChildSlot);
      }
    }
    

    实际上也只是执行了updateChild(),这个方法内部负责页面内容的构建。

    传入的参数有:

    • _child,是个Element对象,表这个根Element的子Element,它其实就是与widget.child对应的element,此时为null,updateChild()工作完成后赋值。

    • widget.child,其实就是我们在runApp时传入的MyApp(),也即我们页面要显示的widget。

    • _rootChildSlot,一个静态slot对象,用于第一次往根Element插入子Element时的配置信息。

    static const Object _rootChildSlot = Object();
    

    updateChild() 内部实现

    // Element类
    Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
      if (newWidget == null) {
        if (child != null)
          deactivateChild(child);
        return null;
      }
      final Element newChild;
      if (child != null) {
        bool hasSameSuperclass = 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);
          newChild = child;
        } else {
          deactivateChild(child);
          newChild = inflateWidget(newWidget, newSlot);
        }
      } else {
        newChild = inflateWidget(newWidget, newSlot);
      }
      return newChild;
    }
    

    首先,newWidget为null时,可以理解为要显示一个空白的页面,那么就需要清空之前页面中的元素,调用deactivateChild()会解除child与父element的绑定、detach该child下所有子RenderObject、并将该element加入到BuildOwner的_inactiveElements列表中等待销毁或重用。

    接下来分两种情况,一是当之前页面中已经有显示的内容了(child不为null),二是未显示过内容,是一个新页面(符合第一帧的情况)

    • child != null,判断下该child的widget配置信息是否没有变化,没有的话,如果slot有更新,就更新个slot就够了,不需要重新创建element。

      或者当canUpdate()返回true,重写Element的update()方法自定义更新element,否则就执行默认流程,deactivateChild()去掉原来显示内容,然后通过inflateWidget()新建一个element。

    • child = null,通过inflateWidget()新建一个element。

    1_updateChild()运行过程表

    毫无疑问,渲染第一帧时,会走第二种情况,接下来看看inflateWidget()做了些啥。

    // Element类
    Element inflateWidget(Widget newWidget, Object? newSlot) {
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        final Element? newChild = _retakeInactiveElement(key, newWidget);
        if (newChild != null) {
          newChild._activateWithParent(this, newSlot);
          final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
          return updatedChild!;
        }
      }
      final Element newChild = newWidget.createElement();
      newChild.mount(this, newSlot);
      return newChild;
    }
    

    如果该element之前通过global key缓存了,则看看能不能从globalKey列表里直接取出element进行复用,可以复用的话,再调用_activateWithParent()把它挂在到父element的renderObject树上并把该element加入到dirtyElements列表中,等待更新。

    然后调用updateChild()更新子布局。

    不能复用则调用newWidget.createElement()重新生成一个element,再调用它的mount()方法,前文提过,mount方法主要做两件事,一是创建RenderObject,将RenderObject挂在父element上,二是更新该Widget下的子布局

    不同element可能对mount()的实现不同,但是大体上都是这两个步骤,详见SingleChildRenderObjectElement,MultiRenderObjectElement,ComponentElement对mount()的不同实现,这些是日常中主要会用到的element。

    调用mount()方法就形成了一个递归,如此反复,直到所有的子RenderObject都被挂载到树中,至此_rebuild()操作完成。

回到RenderObjectToWidgetAdapter类的attachToRenderTree()方法中

// RenderObjectToWidgetAdapter类
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);
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element!;
}
二、element不为null

element为null,也即渲染第一帧的情况分析完了。当element不为null时,会走element的更新流程,实际上就是更新element的widget配置信息,然后将这个element加入到BuildOwner的_dirtyElements列表中。

接着返回上层

WidgetsFlutterBinding.ensureInitialized()
  ..scheduleAttachRootWidget(app)
  ..scheduleWarmUpFrame();

scheduleAttachRootWidget()操作完成了,回顾下这个方法里面都做了些什么操作,概括来说,其实就是两步操作

  1. 创建根窗口的Widget、Element和RenderObject对象
  2. 构建根窗口下的子布局,将所有子布局的RenderObject都挂载到根RenderObject树上。

scheduleWarmUpFrame()

在上一步,我们已经把RenderObject树准备好了,在这一阶段,需要将准备好的RenderObject树,渲染到屏幕上显示出来,完成第一帧的绘制。

Flutter运行过程(一):一文搞懂Widget更新机制文章里提过,Flutter更新是通过VSync信号回调,通过PlatformDispatcher.scheduleFrame()触发onBeginFrameonDrawFrame,进入flutter的更新流程。

而第一帧的绘制不需要等待VSync信号的监听。

void scheduleWarmUpFrame() {
  ... 
  handleBeginFrame(null);
  handleDrawFrame();
  ...
}

可以看到schudleWarmUpFrame()的具体实现,Flutter在绘制第一帧时直接调用了handleBeginFrame()和handleDrawFrame(),强制渲染根布局。

这两个方法内部的实现,在上一篇文章中已经详细分析过了,所以这里不再花大的篇幅去讨论,

还记得我们在初始化窗口的根RenderObject时,调用了RenderObject.prepareInitialFrame(),把根RenderObject加入到了_nodesNeedingLayout列表,和_nodesNeedingPaint列表中

// RendererBinding类
void initRenderView() {
  assert(!_debugIsRenderViewInitialized);
  assert(() {
    _debugIsRenderViewInitialized = true;
    return true;
  }());
  renderView = RenderView(configuration: createViewConfiguration(), window: window);
  renderView.prepareInitialFrame();
}

// RenderView类
void prepareInitialFrame() {
  // 加入到_nodesNeedingLayout列表
  scheduleInitialLayout();	
  // 加入到_nodesNeedingPaint列表
  scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
}

所以在layout、paint阶段,就会遍历这两个列表,执行每个RenderObject的layout和paint过程,最后将计算好的视图合成成一个layer,发送给GPU渲染。

以上就是Flutter渲染第一帧的全部内容。

最后的最后

研究第一帧的渲染过程,也是我学习和熟悉Flutter的过程,只有对Flutter的运行过程充分熟悉,才能更好地理解Flutter的特性及其原理,希望对你有所帮助。

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)

  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!

拜托拜托,谢谢各位同学!