Flutter页面渲染流程分析

361 阅读5分钟

“我正在参加「掘金·启航计划」”

我们都知道Flutter是从main函数开始,并且通过runApp来启动页面,那么从runApp开始到页面真正显示出来,这期间都具体做了什么,对于Flutter中的最重要的三棵树Widget,Element以及RenderObject,它们之间的关系到底是什么样的。接下来就将揭开它的神秘面纱。接下来就拿一个很简单的页面为例,通过断点的方式一步步跟踪它的实现。我们的调试代码如下

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

    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return const ColoredBox(color: Colors.blue);
      }
    }

根Widget,Element,RenderView的初始化过程

在runApp处断点进入可以发现runApp的方法很简单,只有三行代码

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

这里其实就是通过ensureInitialized获取WidgetsFluttingBinding对象实例,然后分别调用了该实例的scheduleAttachRootWidget以及scheduleWarmUpFrame方法。那么我们就先看看scheduleAttachRootWidget做了什么

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

这里其实就是调用Timer.run(),然后在其回调中调用attachRootWidget方法,这个run方法其实就是一个异步的方法(Runs the given [callback] asynchronously as soon as possible)。所以我们重点关注这个callback的执行。

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();
  }
}

可以看到在第四行创建了根Widget,类型为RenderObjectToWidgetAdapter,并且调用了它的attachToRenderTree方法,创建了根Element,类型为RenderObjectToWidgetElement 。很明显我们需要看看这个RenderObjectToWidgetAdapter的定义。

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
 ...
  RenderObjectToWidgetAdapter({
    this.child,
    required this.container,
    this.debugShortDescription,
  }) : super(key: GlobalObjectKey(container));

  ///传递过来的子widget,这里是MyApp
  final Widget? child;

  /// 由当前Widget创建的Element父级RenderObject,是传递过来的,这里是RenderView
  final RenderObjectWithChildMixin<T> container;

  ///提供了创建Element方法
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

  //提供了创建渲染对象的方法
  @override
  RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;

  @override
  void updateRenderObject(BuildContext context, RenderObject renderObject) { }

  
  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!;
  }

  @override
  String toStringShort() => debugShortDescription ?? super.toStringShort();
}

在30行调用了createElement方法,所以创建了RenderObjectToWidgetElement ,然后在35行调用了它的mount方法,所以我们就看看这个mount方法到底做了什么

class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement {
 
  RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> super.widget);

  Element? _child;

  static const Object _rootChildSlot = Object();

...

  @override
  void mount(Element? parent, Object? newSlot) {
    assert(parent == null);
    super.mount(parent, newSlot);
    _rebuild();
    assert(_child != null);
  }

...

  // When we are assigned a new widget, we store it here
  // until we are ready to update to it.
  Widget? _newWidget;

...

  @pragma('vm:notify-debugger-on-exception')
  void _rebuild() {
    try {
      _child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
    } 
    ...
   }
...
}

可以看到先执行了父级的mount方法,并且传入的参数都为null。然后在执行_rebuild方法(暂时定为break1)。我们继续追踪mount方法,接下来调用的是RootRenderObjectElement中的该方法,这个element的定义 abstract class RootRenderObjectElement extends RenderObjectElement

20.png 从这里可以看出传递的参数确实为null,并且继续调用父级的该方法,也就是RenderObjectElement,它的mount方法

21.png 他也是调用了父级(Element)的mount方法,这个方法里最重要的是为深度_depth赋值为1.感兴趣的可以继续,这里就不深入了。5817行调用了createRenderObject方法,注意这里的this为RenderObjectToWidgetElement ,也就是调用了它的createRenderObject方法,通过之前的分析就是传入的顶层渲染对象RenderView,这里的debug结果印证了这一点。随后会调用attachRenderObject方法来建立树的依赖关系,因为是跟element所以该方法中的执行结果为null

30.jpg

其实主要是三个方法

  • _findAncestorRenderObjectElement:从元素树中向上查找首位RenderObjectElement类型的节点作为先祖节点(_ancestorRenderObjectElement)。
  • insertRenderObjectChild:先祖元素节点的方法,将自身持有的 renderObject 对象插入渲染树中。从这里可以看出 渲染树 节点是由 元素 进行维护的
  • _findAncestorParentDataElement:寻找首位类型为ParentDataElement的节点,如果非空执行_updateParentData方法来更新渲染对象的parentData。

对于根Element,这三个方法都不会执行,因为没有parent了。 至此父级的mount方法执行完毕,执行过程

22.png

至此上面提到的三棵树Element与Widget的创建已经介绍完毕,至于根RenderView,则是在创建WidgetsFlutterBinding类初始化时直接创建的(RendererBindinginitRenderView()方法)。 所以可以看出这三棵树之间的关系为

40.png

子组件的加载渲染

之前我们有一个break1还没有说,这里我们继续

41.png 继续看updateChild

image.png 这里的child为null,所以会直接走3682行的inflateWidget方法

image.png 可以看到先调用了newWidget(MyApp)的createElement方法,然后又调用了mount方法。这里的MyApp则是StatelessWidget,它的create方法则是创建了StatelessElement,它的定义如下


abstract class StatelessWidget extends Widget {
 
  const StatelessWidget({ super.key });

  
  @override
  StatelessElement createElement() => StatelessElement(this);


  @protected
  Widget build(BuildContext context);
}

这个element的定义为class StatelessElement extends ComponentElement,这里并没有mount方法,所以我们去ComponentElement中查看

image.png 可以看到MyApp(StatelessElement)的parent正好是我们创建的根widget(RenderObjectToWidgetElement)。

首先调用了父类的super方法则是Element类中的方法,其实就是修改_depth的值

image.png 然后调用了本身的_firstBuild方法,这个方法的内部其实就是调用了rebuild方法

image.png 其实就是调用了performRebuild方法,在ComponentElement中的实现如下

image.png 首先在4968行处调用了build方法,其实就是我们在MyApp中的build方法,在4993处调用了updateChild方法

image.png 根据debug的参数可以看出会直接执行3682行处的inflateWidget方法

image.png 其实关键点就是3948行以及3953行,其实就利用build的内容(ColoredBox)生成element,然后调用mount方法,可以看到这里跟加载MyApp时的逻辑是一样的,下面就不赘述了。这一过程中涉及到的关键类和方法如下

image.png

以上是开篇提到的scheduleAttachRootWidget的流程,此时界面并没有看到我们布局的ColoredBox,那是因为还没有“画”上去,接下来就该runApp中的第三行代码scheduleWarmUpFrame()发挥作用了,由于篇幅原因直接看调用链

image.png