Flutter的渲染过程

如果要搞清楚flutter的渲染流程,必须要知道flutter中最重要的三棵树 widget three element three 以及render tree。他们之间的关系,以及存在的意义。其实渲染的流程也可以说是这三棵树节点对象创建的流程。

一. 三棵树之间的关系

badd906086c64395be03aa0bddd68e1b_tplv-k3u1fbpfcp-watermark.png
从上图可以看的出来 widget treeelement tree是一一对应的关系,但是Render tree 并没有和他们对应。这是因为只有继承自RenderObjectWidgetwidget才会最终被渲染到屏幕上。我们经常用到的一些组件比如Container或者是 Text 这类属于组合widget 因为他们并不是继承自RenderObjectWidgetContainer内部其实是由 Padding ColoredBox Align等组合而成,而Text内部其实RichText

  • Widget 用来描述View的最终展示形态,比如尺寸多大,是什么颜色的 距离屏幕边缘距离是多少等等
  • Element 与widget一一对应 为了保持树的稳定形态,同时持有widgetrenderObject
  • Render 渲染树对象,由他最终提供渲染方法 比如 markNeedsLayout performLayout markNeedsPaint paint 等方法,渲染在屏幕上,与 element 并不是一一对应。

二. 对象创建的过程

1. Widget对象的创建

Widget对象的创建非常简单其实就是你在调用构造函数的时候,比如下面的代码

void main() {
   runApp(MyApp());
 }
复制代码

MyApp()就是这个Widget对象创建,非常简单。

2. Element对象的创建

跟进源码我们会发现每一个Widget类都有一个createElement()方法,那么这个方法的调用时机就是 Element对象创建的时候。我们再runApp()这个方法里面发现有这么一段代码,为了方便了解这个过程我把一些无关的代码省略掉

 RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
    ......
     element = createElement();
   .....
     element!.mount(null, null);
  .....
  
 
 return element!;
}
复制代码

所以在这个方法中调用了createElement(),根Element创建成功。紧接着调用mount方法将这个Element插入到对应的Element tree中。然后调用 inflateWidget()方法创建 在这个方法里面会调用子elementcreateElement()方法,然后子element 继续调用mount方法,以此类推,element tree最终形成。

简单的梳理一下流程

  1. attachToRenderTree() 方法调用createElement()创建出rootElement
  2. rootElement对象调用mount()方法将rootElement插入到树的节点中,并调用inflateWidget()
  3. inflateWidget()方法中 子widget调用createElement()mount()重复以上步骤,直至element tree创建完成
 Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ....
    final Element newChild = newWidget.createElement();
    .....
     newChild._activateWithParent(this, newSlot);
   ....
    newChild.mount(this, newSlot);
   ....
    return newChild;
  }
复制代码

3. Render对象的创建

Element一样,RenderObject类有一个很重要的方法createRenderObject(),也就是说调用这个方法的时机就是创建RenderObject对象的时候。前面说过只有继承自RenderObjectwidget最终才会被渲染。才会创建RenderObject对象。flutter中大多数widget的类型都有与之对应的element如下图

20210624171313.jpg

所以真正继承RenderObjectElement的对象才会真正的去调用createRenderObject()
跟踪源码我们发现其实是在element对象插入到树节点的时候也就是调用mount的时候创建了对应的renderObject对象。

void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
     ......
    _renderObject = widget.createRenderObject(this);
    ......
    attachRenderObject(newSlot);
    _dirty = false;
  }
复制代码

三. build方法的执行流程

element中有一个很重要的方法markNeedsBuild()这个方法主要做了两件事

  1. 标记elementdirty
  2. 把当前的element对象放到全局_dirtyElements

执行流程

  1. element插入到对应的树节点之后,当前element被标记为active状态,然后执行activate()
  2. activate()方法中执行markNeedsBuild()将该对象标记为dirty
  3. 之后系统会调用_handleBuildScheduled() scheduleWarmUpFrame() scheduleFrame() window.scheduleFrame();等一系列方法,其目的是为了注册vsync信号,注册完成等待下一次vsync的到来
  4. 当下一次vsync到来以后会触发 WidgetsBinding.drawFrame buildScopebuildScope()这个方法中会获取到刚才被标记为dirty的列表,也就是_dirtyElements 对该列表逐一进行rebuild()
  5. rebuild()方法中,调用对应的performRebuild(),最终调用Widget build() => widget.build(this); 流程结束

在上面的流程中,有个细节就是下一次的vsync什么时候到来?
我们一般用于评判UI是否卡顿,使用了一个叫做 fps(Frames Per Second)的指标,每秒需要传输的帧数。我们一般用 60fps来衡量,如果低于60 那就会产生卡顿,越低卡顿就越明显。这是因为大多数设备的刷新频率就是 60fps,也就是每一秒处理60帧的数据,所以一帧数据处理的时间大概就是 1/60s 也就是0.0166s。这个vsync就是开始渲染第一帧的时候设备发出的信号。所以如果设备的刷新频率是 60fps,那么理想情况下,下一次vsync信号就是0.0166秒之后到来。

四. 总结

了解了widget element render对象创建的时机以及build()方法的执行流程以后,我们把所有的点都穿起来可以得出下面的流程

  1. 首先创建widget对象,然后在attachToRenderTree方法中创建rootElement
  2. rootElement 插入到element tree,并创建 renderObject对象,紧接着执行 markNeedsBuild()方法
  3. 通过build方法获取到下一个子节点的widget,然后执行inflateWidget()创建element对象
  4. 执行mount()方法,插入element tree中,创建对应的renderObject对象,然后再执行这个elementmarkNeedsBuild()方法,重复上面的步骤,直至完成三棵树的创建。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享