Flutter:架构与运行机制

1,482 阅读17分钟

关于Flutter

Flutter是一个由谷歌开发的开源UI框架,用于为Android、iOS、 Windows、Mac、Linux、Fuchsia开发应用.

fsen

Flutter采用自绘引擎的方式进行UI渲染,也就是底层会调用Skia、OpenGL这种跨平台的绘制引擎直接为GPU提供绘制数据,所以其性能会大于等于原生组件的性能.

采用自绘引擎的方式会带来两个好处:一个是平台无关性;一个是操作系统版本的无关性.

平台无关性是说在Android、ios、Windows、Mac、Linux、Fushia等操作系统中UI会有良好的一致性体验.这个很好理解,因为各个平台的显示原理与OpenGL引擎的表现是一致的.

操作系统版本无关性就是说操作系统的不同版本对UI的绘制(系统提供的功能除外)几乎没有影响,因为Flutter的UI绘制并不是使用与操作系统版本具有强相关的组件进行绘制的,也就不会有因为操作系统升级而导致的组件渲染差异所带来的一些奇奇怪怪的bug困扰.

Flutter架构

可以把整个Flutter架构分为上下两部分,上层为Framework,下层为Engine. 如下图:

frame_flutter

上层Framework是由Dart语言和用Dart编写的Flutter框架组成;下层Engine是用C++实现的引擎,是连接上层Framework和底层操作系统的桥梁,主要包含Dart运行时和操作系统环境为运行时提供的接口(Skia渲染引擎和文字排版引擎).

在Flutter架构中,有一个在Framework和Engine引擎之间起到连接作用的关键类:即Window类.

Window类是定义在Dart:ui中的,其官方解释是:The most basic interface to the host operating system's user interface.也就是宿主操作系统提供的最基本的接口.

Window类提供了屏幕尺寸、事件回调、图形绘制接口以及其他一些核心服务.

这种架构完全可以类比一下React架构,如下图

react

React架构也可以分为上下两部分,上层为Framework,下层为Engine.

上层Framework是由Javascript语言和用Javascript编写的React框架组成;下层Engine是用C++实现的引擎,也是连接上层Framework和底层操作系统的桥梁,主要包含JS运行时和操作系统为运行时提供的接口(也就是宿主环境,比如浏览器,浏览器里封装了对操作系统Network、GPU等的调用).

在React架构中,也有一个在Framework和Engine引擎之间起到连接作用的关键对象:window对象.

window对象是宿主环境为javascript语言提供的,其官方解释是:The window object is supported by all browsers. It represents the browser's window.All global JavaScript objects, functions, and variables automatically become members of the window object. Global variables are properties of the window object.Global functions are methods of the window object.Even the document object (of the HTML DOM) is a property of the window object.也就是宿主浏览器提供的功能接口.

window对象提供了窗口尺寸、事件注册、网络请求等一些服务.

在浏览器中,基于window对象,可以用javascript编写出React、Vue、Angular等不同的UI框架.

同样的,在移动端中,基于Windows类,也可以用Dart编写出其他的UI框架来代替Flutter,只要你愿意.

Flutter的渲染机制

因为Flutter采用的自绘引擎,所以它的渲染机制是和底层的显像原理相关的

visual

在计算机系统中,显示器显示的图像帧来自于GPU缓存,GPU缓存中的图像帧是GPU计算的结果,GPU计算需要的数据是CPU通过总线传递过来的.

显示器每显示完一帧图像就会发送一个垂直信号(Vsync)给视频控制器,视频控制器就会从GPU缓存中读取下一帧图像并传递给显示器显示,如果一个显示器是60Hz的刷新频率,也就是说这个显示器每秒会发送60次这样的垂直信号(Vsync).同样,如果GPU在一秒内可以吐出90帧的图像,那么显示输出就是90fps.

而Flutter渲染所关注的就是在下一次垂直信号到来之前(两次Vsync之间)尽可能快的计算出下一帧图像数据并交给GPU.

换句话说,Flutter的渲染动作完全是依靠垂直信号(Vsync)来驱动的(除了第一次),记住这一点很重要,因为不论是通过setState更新UI还是UI动画的渲染,都是依靠不断请求Vsync来驱动进行的.

vsync

如上图,Flutter请求来的垂直同步信号(Vsync)被GPU传递到UI线程里,UI线程里的Dart运行时接受到Vsync后会进行一个被称为渲染流水线(Rendering Pipeline)的处理过程来生成一种叫场景的图像数据(An opaque object representing a composited scene),场景再被送到GPU线程里(期间可能会多次经过硬件加速处理的过程)供Skia引擎处理成GPU可使用的数据,这些数据最后经由OpenGL送给GPU进行渲染成帧,并最终由视频控制器交给显示器显示.

渲染流水线

当把请求过来的Vsync传递到UI线程里时会触发一个渲染流水线(Rendering Pipeline) 如下图

render-pipeline

渲染流水线会按顺序进行一系列阶段并最终产生一个Scene并发送给GPU:

  • Animate(动画阶段):这个阶段要运行一些和动画相关的瞬时回调(transient frame callbacks),因为这里要和普通的重绘区别对待,动画里会涉及是否要再发起新的一帧以及何时发起的计算,这也是第一步需要确定的.
  • Microtasks(微任务阶段):动画阶段会产生一些微任务,这个阶段执行那些微任务.
  • Layout(布局阶段):这个阶段会重新计算所有被标记为dirty的RenderObject的尺寸和大小.
  • CompositingBits(合成标记阶段):对RenderObject进行标记,是否需要在下次的渲染流水线中重绘
  • Paint(绘制阶段):使用PaintingContext对所有脏RendeObject进行重新绘制,这个阶段会产生Layer Tree
  • Compositing(合成阶段):这个阶段会把Layer Tree转换成场景并发送到GPU
  • Semantics(语义阶段):更新发送语义
  • Finalization(完结阶段):主要进行一些收尾工作,比如卸载所有不处于active状态的element

当Flutter需要更新UI的时候,会请求一次Vsync信号来触发一次重新渲染.

Vsync信号到达engine层时,engine会调用 window.onBeginFrame回调,此时Rendering Pipeline开始进行Animate和Microtasks阶段.

在Animate和Microtasks阶段完成后,engine会接着调用window.onDrawFrame回调,该回调会进行Rendering Pipeline剩下的阶段.

pipeline

以上就是一个完整的渲染流水线,由此可见Rendering Pipeline的8个阶段是在onBeginFrame和onDrawFrame这两个回调中触发的.

再次强调Flutter的渲染动作完全是依靠垂直信号(Vsync)来驱动的,不论是通过setState更新UI还是UI动画的渲染,都是依靠不断请求Vsync来驱动进行的.但除了第一次,即入口函数里的runApp,因为第一次渲染不会请求Vsync而是直接进入流水线渲染.

下面通过runApp入口函数来具体看一下渲染流水线的各个阶段.

runApp函数

runApp函数的官方解释是Inflate the given widget and attach it to the screen.The widget is given constraints during layout that force it to fill the entire screen.

也就是说调用runApp的结果就是展开一个widget并挂载到屏幕上,并且会给这个被挂载的widget一个铺满整个屏幕的约束.

runApp函数在程序中可以被调用多次.当再次被调用时,原来挂载到屏幕上的根widget会被卸载掉,并替换上新传入的根widget,这两个widget之间仍然会进行diff算法比较只进行边际增量的更新.

这是Flutter规定的内置入口,除非自己实现一个Xlutter框架可以不走这个入口.

但以上只是runApp运行的结果,并没有体现出渲染流水线的具体过程来

所以需要点开runApp源码查看具体做了哪些事情

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

runApp方法里很简洁,总结起来一共做了三件事情:

  • 初始化了WidgetsFlutterBinding类的实例
  • 调用了实例的scheduleAttachRootWidget方法
  • 调用了实例的scheduleWarmUpFrame方法

接着再查看WidgetsFlutterBinding类的初始化过程

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

发现ensureInitialized并不是一个命名构造函数,而是一个静态方法,这个静态方法实现了WidgetsFlutterBinding的单例模式.

在静态方法ensureInitialized内部调用了WidgetsFlutterBinding的默认构造函数,并返回WidgetsFlutterBinding实例.

这里有很多值得研究的地方,当调用WidgetsFlutterBinding()的时候,其实是进行了很多的绑定动作的.

首先观察到WidgetsFlutterBinding继承了BindingBase类,并且mixin了7种binding.BindingBase类的默认构造函数会调用initInstances方法,而这7种binding也都重写了initInstances方法,也就是说在调用WidgetsFlutterBinding()构造函数初始化的时候,这7个binding所重写的initInstances也都会执行并且只一遍来执行相应的初始化绑定操作.

其次还要注意到这7种mixin混入的顺序,先混入的binging要比后混入的binding更基础,因为后混入的binding会使用或重写先混入的binding中的方法,由此可以看出widgetsBinding是更higher-level的库,所以说WidgetsFlutterBinding的注释中会有这么一句话

WidgetFlutterBinding

即应用程序基于Widgets framework的具体绑定.也可以大体可以猜到这个类为什么叫WidgetsFlutterBinding而不叫RenderingFlutterBinding或其他的名字,因为WidgetsBinding之前的初始化都是直接和渲染层(render layer)打交道的,在渲染层之上你也可以实现其他的更高级的库,比如叫个Xidgets、Yidgets啥的.事实上Flutter也确实提供了只到RendererBinding绑定的类以供使用.

class RenderingFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, SemanticsBinding, PaintingBinding, RendererBinding {
  RenderingFlutterBinding({ RenderBox root }) {
    assert(renderView != null);
    renderView.child = root;
  }
}

Binding Mixins

通过这7种mixin的名字也可以知道它们的主要作用就是在初始化的过程中进行一些绑定操作,这些绑定主要就是通过绑定window实例上的某些回调来和Engin层进行通信,前面讲过window是Framework层和Engine层的粘合剂.

要绑定window上的回调就要先引用这个window对象,所以在BindingBase中可以看到以下这段代码

import 'dart:ui' as ui show saveCompilationTrace, Window, window;
...
abstract class BindingBase {
...
ui.Window get window => ui.window;
...
}

这段代码在BindingBase中设置了一个window getter,引用了dart:ui中的window实例,这个window getter可以在所有的binding中被使用.

接下来

  • GestureBinding

    这个binding看名字就可以知道,主要就是进行一些手势相关的绑定.绑定了window对象上的 onPointerDataPacket 回调

      window.onPointerDataPacket = _handlePointerDataPacket;
    
  • ServicesBinding

    这个binding主要是对平台消息进行监听并对消息进行转发

       window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
    
  • SchedulerBinding

    这个binding很重要,渲染流水线中的window.onBeginFrame回调和window.onDrawFrame回调,还有发起一帧的请求都是在这里注册的

    //注册onBeginFrame和onDrawFrame回调
      @protected
      void ensureFrameCallbacksRegistered() {
        window.onBeginFrame ??= _handleBeginFrame;
        window.onDrawFrame ??= _handleDrawFrame;
      }
    
    //请求一帧
      void scheduleFrame() {
        if (_hasScheduledFrame || !framesEnabled)
          return;
        ensureFrameCallbacksRegistered();
        window.scheduleFrame();
        _hasScheduledFrame = true;
      }
    

    同时它还为渲染流水线提供流程管理的功能,为了协调“发起一帧”这个动作与Rendering Pipeline中8个阶段的执行顺序(因为一次Rendering Pipeline可能没有执行完,app就又发起了一次帧请求),SchedulerBinding内部定义了5种不同的Schedule状态

    • idle
      • 空闲状态
    • transientCallbacks
      • 瞬时回调被执行状态,执行Animate阶段的函数都被注册进transientCallbacks里面
    • midFrameMicrotasks
      • 瞬时回调返回的微任务被执行状态
    • persistentCallbacks
      • 持久回调被执行状态.执行Layout、Compositing bits、Paint、Compositing、Semantics、Finalization这些阶段的函数都被注册进了persistentCallbacks里面
    • postFrameCallbacks
      • 收尾回调被执行,这里的函数主要进行一些clean动作并为下一次帧请求做好准备

    当onBeginFrame的回调被调用后渲染流水线进入的Animate阶段和Microtasks阶段对应的就是transientCallbacks状态和midFrameMicrotasks状态,而persistentCallbacks状态对应的就是onDrawFrame的回调被调用后渲染流水线进入的剩下6个阶段.

    在ensureVisualUpdate函数(这个函数是在UI需要重绘时触发的)中可以看出只有当Schedule阶段处于idle和postFrameCallbacks状态(也就是)时才会发起新的一帧请求

    void ensureVisualUpdate() {
      switch (schedulerPhase) {
        case SchedulerPhase.idle:
        case SchedulerPhase.postFrameCallbacks:
          scheduleFrame();
          return;
        case SchedulerPhase.transientCallbacks:
        case SchedulerPhase.midFrameMicrotasks:
        case SchedulerPhase.persistentCallbacks:
        return;
      }
    }
    
  • PaintingBinding

    绑定绘制库,做一些和图片缓存相关的工作

  • SemanticsBinding

    语义层的绑定,平台可能启动的其他辅助功能

    _accessibilityFeatures = window.accessibilityFeatures;
    
  • RendererBinding

    这个binding也相当重要,主要做了一些和渲染有关的事情.

    在Flutter应用中凡是屏幕中能看到视觉渲染效果都是用render tree渲染出来的, render tree会被加工成layer tree,而layer tree又会被转化成一个scene输送给GPU.

    render tree是由多个render object组成的一棵树结构,而正是在这个binding里create了这棵树的根节点renderView.

    这个根节点renderView里面有个叫compositeFrame的方法,这个方法就是在渲染流水线的Compositing阶段把layer tree转化成scene并输送给GPU的方法.

    其中的buildScene和render分别对应了转化和输送阶段.

    void compositeFrame() {
      Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
      try {
        final ui.SceneBuilder builder = ui.SceneBuilder();
        final ui.Scene scene = layer.buildScene(builder);
        if (automaticSystemUiAdjustment)
          _updateSystemChrome();
        _window.render(scene);
        scene.dispose();
      } finally {
        Timeline.finishSync();
      }
    }
    

    这个binding还初始化了一个PipelineOwner实例来管理render tree上的所有节点.

    这个PipelineOwner实例只会被初始化一次,并会被所有render object所共同持有,每个render object都会有一个owner字段引用该实例.

    PipelineOwner主要维护了三个list

    • _nodesNeedingLayout
      • 需要重新layout的脏render object
    • _nodesNeedingPaint
      • 需要重新paint的脏render object
    • _nodesNeedingCompositingBitsUpdate
      • 被标记的render object以供判断

    当脏render objects需要重新layout或paint的时候,会调用PipelineOwner实例的onNeedVisualUpdate回调,这个回调会间接触发ensureVisualUpdate方法来请求一帧并触发一次rendering pipeline.

    而一次rendering pipeline周期中onDrawFrame回调所调用的persistentCallbacks也是在这个binding里注册的

    //注册persistentCallbacks
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    ...
    ...
    ...
    //定义的persistentCallbacks回调
    void _handlePersistentFrameCallback(Duration timeStamp) {
      drawFrame();
      _mouseTracker.schedulePostFrameCheck();
    }
    @protected
    void drawFrame() {
      assert(renderView != null);
      pipelineOwner.flushLayout();
      pipelineOwner.flushCompositingBits();
      pipelineOwner.flushPaint();
      if (sendFramesToEngine) {
        renderView.compositeFrame(); // this sends the bits to the GPU
        pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
        _firstFrameSent = true;
      }
    }
    

    这里面的pipelineOwner.flushLayout()、pipelineOwner.flushCompositingBits()、pipelineOwner.flushPaint()、renderView.compositeFrame()、pipelineOwner.flushSemantics()分别对应了render pipeline中的Layout、CompositingBits、Paint、Compositing、Semantics阶段

    其中pipelineOwner.flushLayout()、pipelineOwner.flushCompositingBits()、pipelineOwner.flushPaint()这三个方法会循环遍历_nodesNeedingLayout、_nodesNeedingPaint、_nodesNeedingCompositingBitsUpdate这三个list对元素进行操作(performLayout、PaintingContext.repaintCompositedChild、_updateCompositingBits方法).

      void flushLayout() {
        ...
        while (_nodesNeedingLayout.isNotEmpty) {
          final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
          _nodesNeedingLayout = <RenderObject>[];
          for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
            if (node._needsLayout && node.owner == this)
              node._layoutWithoutResize();
          }
        }
      }
    
      void flushCompositingBits() {
        ...
        for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
          if (node._needsCompositingBitsUpdate && node.owner == this)
            node._updateCompositingBits();
        }
      }
      void flushPaint() {
        final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
        _nodesNeedingPaint = <RenderObject>[];
        // Sort the dirty nodes in reverse order (deepest first).
        for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
          if (node._needsPaint && node.owner == this) {
            if (node._layer.attached) {
              PaintingContext.repaintCompositedChild(node);
            } else {
              node._skippedPaintingOnLayer();
            }
          }
        }
    }
    

    到这里Flutter的渲染流水线需要的绑定和回调已经都注册完成,一个完整的流水线可以运行起来了.

    pipe

    可以发现Rendering Pipeline就是对Render Tree进行加工操作并最终返回一个Scene的过程,完全看不到Widget和Element的影子,这也再次印证了RenderFlutterBinding是直接和渲染层打交道的,可以在这之上编写一个更higher-level的库来实现一个UI框架.

    如果说Flutter是一个响应式的UI框架,Render Tree代表UI,那么Widget和Element就代表了响应式,就好比React中VirtualDOM和Dom之间的关系.

  • WidgetsBinding

    这层的binding相比前面的binding是在更高一层上做了初始化操作,前面的binding主要涉及render layer,而这层binding主要涉及widgets layer.也可以这么说,这层binding做了对render tree的代理(即widget)所封装的响应式逻辑的初始化.

    WidgetsBinding在初始化的时候基本上就做了两件事情:

    • 实例化了一个所有Element都持有并共享的BuildOwner,每个Element实例都通过owner字段引用该实例.这点和PipelineOwner差不多.一棵Element树只能有一个BuildOwner,但也可以多建几个BuildOwner来管理离屏的Element树

      •  _buildOwner = BuildOwner();
        
    • 绑定了window上的两个回调

      •   window.onLocaleChanged = handleLocaleChanged;
          window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
        

    BuildOwner主要作用就是跟踪维护改动的widgets和elements.

    • 它内部维护了两个list:

      • _InactiveElements
        • 严格来说这是一个内部定义了一个list的类
        • 存储处于inactive状态的element
      • _dirtyElements
        • 存储需要重新被构建的element
    • 还定义了两个重要方法

      • scheduleBuildFor

        • 由element实例调用,把自己添加进_dirtyElements
          void scheduleBuildFor(Element element) {
            if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
              _scheduledFlushDirtyElements = true;
              onBuildScheduled();
            }
            _dirtyElements.add(element);
            element._inDirtyList = true;
          }
        

        在StateFulWidget中调用setState刷新UI的时候其实调用的就是这个方法,setState把一个element插入到_dirtyElements中,并发出一帧的请求来重新build在_dirtyElements中的元素

        //setState方法
          void setState(VoidCallback fn) {
            ...
            _element.markNeedsBuild();
          }
        //markNeedsBuild方法
          void markNeedsBuild() {
            _dirty = true;
            owner.scheduleBuildFor(this);
          }
        
      • buildScope

        • 重构_dirtyElements中的element,会根据element的类型不同调用不同performBuild
        • 这个方法一般会在onDrawFrame的回调里调用,因为这个WidgetsBinding重写了RendererBinding里的drawFrame方法
            @override
            void drawFrame() {
              ...
              buildOwner.buildScope(renderViewElement);
              super.drawFrame();
              buildOwner.finalizeTree();
            }
        

        可以发现在PipelineOwner调用drawFrame之前会调用buildScope方法,在PipelineOwner调用drawFrame之后会调用finalizeTree方法,也就是说其实在渲染流水线的Layout阶段之前其实还有一个Build阶段,这个Build阶段主要就是用来更新widgets和render object的.

        所以当用widgets framework对渲染层进行包装形成响应式的时候一个更完整的渲染流水线应该是这样的

        newpipe

以上一个Flutter应用基于Widgets Framework的初始化工作就完成了,紧接着在runApp中做的第二件事情就是调用scheduleAttachRootWidget函数

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

    void attachRootWidget(Widget rootWidget) {
      _readyToProduceFrames = true;
      _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
        container: renderView,
        debugShortDescription: '[root]',
        child: rootWidget,
      ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
  }

这里的名字起的很有意思,attachRootWidget、attachToRenderTree和传递的参数名字:container、child.

按字面意思widget是子,render object是容器,所以要attatch widget.从这种命名上可以窥见Flutter作者对render object和widget的心理定位:widget只是用来配置render object的附属.

所以说如果没有响应式这一层Framework,Flutter中的树结构应该是很直观和很好理解的,就一棵render tree,这个render tree上的每个节点都有引擎如何渲染的描述信息,但是在加入了响应式这一层Framework后情况就变得有些复杂了.

有了Widgets Framework这一层后Flutter和我们直接打交道的就不再是渲染层而是widgets层.就好像用了React后,我们就很少和真实的DOM树打交道也很少会调用appendChild、createElement这样的接口,而是直接使用Componet这样的组件.Component就相当于Widget,是个配置对象,这个配置对象会被createElement转化成VirtualDOM,最后通过render函数渲染成真实的DOM,也就是说VirtualDOM是Component和DOM之间的桥梁.

同样的,在FLutter中,Element就是Widget和RenderObject之间的桥梁,Widget也会被一个名为createElement的函数转化成一个Element,最后通过buildScope把配置信息更新到RenderObject上.

在attachToRenderTree中,就是创建了一个类型为RenderObjectToWidgetElement的Element根结点,然后再调用buildScope函数把element上的widget信息更新到render tree中.

    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);
      });
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }

render tree中的渲染信息已万事俱备,就差一个垂直同步信号来一场渲染流水线render出画面来,但是在runApp里已经迫不及待了,不等垂直信号而是调用了scheduleWarmUpFrame函数直接开启一个Rendering Pipeline渲染出首屏.

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