渲染

绘制渲染

三棵树

https://flutter.dev/docs/resources/architectural-overview#layout-and-rendering

image

image

Widget Tree,Element Tree 以及 RenderObject Tree 。根据它们的功能我将它翻译成模型树,状态树和渲染树,也正是通过这三棵树维护起了整个应用的视图数据。

开发者通过Widget配置,Framework通过比对Widget配置来更新Element,最后调度RenderObject Tree完成布局排列和绘制。

  • Widget Tree

存放属性的描述信息,更像是一个Model。同一个Widget可以同时描述多个渲染树中的节点,但是它是不可修改的,因此它只会被创建或销毁。

Widget是为Element描述需要的配置, 负责创建Element,决定Element是否需要更新。Flutter Framework通过差分算法比对Widget树前后的变化,决定Element的State是否改变。当重建Widget树后并未发生改变, 则Element不会触发重绘,则就是Widget树的重建并不一定会触发Element树的重建。

  • Element Tree

存放上下文状态信息,同时持有 Widget和RenderObject的引用。像是一个Controller控制着状态的更新(initial, mount,amount,activate,deactivate,update)。

Element表示Widget配置树的特定位置的一个实例,同时持有Widget和RenderObject,负责管理Widget配置和RenderObject渲染。Element状态由Flutter Framework管理, 开发人员只需更改Widget即可。

  • RenderObject Tree

实现了layout和paint事件,是最终渲染的View视图。

RenderObject表示渲染树的一个对象,负责真正的渲染工作,比如测量大小、位置、绘制等都由RenderObject完成。

渲染库(Rendering)

Flutter的控件树在实际显示时会转换成对应的渲染对象(RenderObject)树来实现布局和绘制操作。渲染库主要提供的功能类有:

abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { ... }
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
abstract class RenderBox extends RenderObject { ... }
class RenderParagraph extends RenderBox { ... }
class RenderImage extends RenderBox { ... }
class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>,
                                        RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>,
                                        DebugOverflowIndicatorMixin { ... }

RendererBinding是渲染树和Flutter引擎的胶水层,负责管理帧重绘、窗口尺寸和渲染相关参数变化的监听。

RenderObject渲染树中所有节点的基类,定义了布局、绘制和合成相关的接口。

RenderBox和其三个常用的子类RenderParagraph、RenderImage、RenderFlex则是具体布局和绘制逻辑的实现类。

控件树中的每个控件通过实现RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject方法来创建对应的不同类型的RenderObject对象,组成渲染对象树。


渲染机制—UI线程

Flutter渲染机制—UI线程–详细

渲染机制—GPU线程

Flutter渲染机制—GPU线程–详细

Flutter 视图绘制

Flutter有别于其他跨平台开发的一大特点是它自带UI组件和渲染器,而不是通过一些Bridge去做平台适配。其中自带UI组件在Flutter的Framework层而渲染器在Engine层,那么Google工程师面临的问题是如何尽可能快的(在VSync信号间隔内)完成这次传递?

Flutter app只有在状态发生变化的时候需要触发渲染流水线。当你的app什么都不做的时候是不需要重新渲染页面的。所以,Vsync信号需要Flutter app去调度。比如我们都知道如果你的某个页面需要发生变化的时候有可能会调用State.setState(),这个调用Flutter框架最终会发起一个调度Vsync信号的请求给底层。然后底层会在Vsync信号到来的时候驱动渲染流水线开始运作,最后把新的页面显示到屏幕上。

image

image

从UI绘制的整体流程来看,从用户的输入①到界面上动画的进度更新②,然后开始视图数据的build③,通过Layout④来确定视图的Position和Size,接下来是视图数据的Paint⑤和Composite⑥,最后是将合成后的视图数据进行"光栅化"处理使它真正的变成一个个像素填充的数据并提交给GPU。

整个渲染流水线是运行在UI线程里的,以Vsync信号为驱动,在框架渲染完成之后会输出layer tree。 layer tree被送入engine,engine会把layer tree调度到GPU线程,在GPU线程内合成(compsite)layer tree,然后由Skia 2D渲染引擎渲染后送入GPU显示。

Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。

GPU线程通过skia向GPU硬件绘制一帧的数据,GPU将帧信息保存到FrameBuffer里面,然后视频控制器会根据VSync信号从FrameBuffer取帧数据传递给显示器,从而显示出最终的画面。

image

https://flutter.dev/docs/resources/architectural-overview#from-user-input-to-the-gpu

Animate

遍历_transientCallbacks,执行动画回调方法;

Build

在这个阶段Flutter,在这个阶段那些需要被重新构建的Widget会在此时被重新构建。也就是我们熟悉的StatelessWidget.build()或者State.build()被调用的时候。

对于dirty的元素会执行build构造,没有dirty元素则不会执行,对应于buildScope()

Layout

布局的计算

计算渲染对象的大小和位置,此时是RenderObject.performLayout()被调用的时候。

对应于flushLayout(),这个过程可能会嵌套再调用build操作;

因为Flutter极大地简化了布局的逻辑,所以整个布局过程中只需要深度遍历一次:

image

渲染对象树中的每个对象都会在布局过程中接受父对象的Constraints参数,决定自己的大小,然后父对象就可以按照自己的逻辑决定各个子对象的位置,完成布局过程。子对象不存储自己在容器中的位置,所以在它的位置发生改变时并不需要重新布局或者绘制。子对象的位置信息存储在它自己的parentData字段中,但是该字段由它的父对象负责维护,自身并不关心该字段的内容。同时也因为这种简单的布局逻辑,Flutter可以在某些节点设置布局边界(Relayout boundary),即当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然:

image

Compositing bits

更新具有脏合成位的任何渲染对象, 对应于flushCompositingBits();

Paint

将绘制命令记录到Layer, 对应于flushPaint();

访问需要绘制的任何渲染对象,在此阶段,渲染对象有机会将绘制命令记录到[PictureLayer],并构建其他合成的[Layer]

布局的绘制,此时是RenderObject.paint()被调用的时候

布局完成后,我们开始真正的绘制。它与layout不同点在于,layout是先有child的size再有parent的size,draw是先绘制parent再child。 布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置,Flutter会把所有对象绘制到不同的图层上:

image

因为绘制节点时也是深度遍历,可以看到第二个节点在绘制它的背景和前景不得不绘制在不同的图层上,因为第四个节点切换了图层(因为“4”节点是一个需要独占一个图层的内容,比如视频),而第六个节点也一起绘制到了红色图层。这样会导致第二个节点的前景(也就是“5”)部分需要重绘时,和它在逻辑上毫不相干但是处于同一图层的第六个节点也必须重绘。为了避免这种情况,Flutter提供了另外一个“重绘边界”的概念:

image

在进入和走出重绘边界时,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响。典型的应用场景就是ScrollView,当滚动内容重绘时,一般情况下其他内容是不需要重绘的。虽然重绘边界可以在任何节点手动设置,但是一般不需要我们来实现,Flutter提供的控件默认会在需要设置的地方自动设置。

Flutter框架分析(七)– 绘制

函数markNeedsPaint()首先做的是把自己的标志位_needsPaint设置为true。然后会向上查找最近的一个isRepaintBoundary为true的祖先节点。直到找到这样的节点,才会把这个节点加入到_nodesNeedingPaint列表中,也就是说,并不是任意一个需要重绘的RenderObject就会被加入这个列表,而是往上找直到找到最近的一个isRepaintBoundary为true才会放入这个列表,换句话说,这个列表里只有isRepaintBoundary为true这种类型的节点。也就是说重绘的起点是从“重绘边界”开始的。

这里的_layer属性就是我们之前说的图层,这个属性只有绘制边界的RenderObject才会有值。一般的RenderObject这个属性是null。

Compositing

将Compositing bits发送给GPU, 对应于compositeFrame();


setState更新

Flutter的setState更新原理和流程

image

深入理解setState更新机制

可见,setState()过程主要工作是记录所有的脏元素,添加到BuildOwner对象的_dirtyElements成员变量,然后调用scheduleFrame来注册Vsync回调。当下一次vsync信号的到来时会执行handleBeginFrame()和handleDrawFrame()来更新UI。

image

State lifecycle

image

Flutter性能优化之局部刷新

利用GlobalKey

class _TestWidgetState extends State<TestWidget> {
  int _count=0;
  GlobalKey<TextWidgetState> textKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextWidget(textKey),///需要更新的Text
          ButtonWidget(
            onPressed: () {
              ///点击button,调用TextWidget的onPressed方法
              ///在TextWidget的onPressed中单独调用TextWidget的setState,
              _count++;
              textKey.currentState.onPressed(_count);
            },

Flutter视图绘制(2)–android绘制流程