03-大前端底层原理|跨平台开发方案-Flutter的渲染原理

3,646 阅读12分钟

[TOC]

前言

Flutter:

  • Flutter是一个使用Dart语言开发的跨平台移动UI框架,通过自建绘制引擎,能高性能、高保真地进行Android和IOS开发。作为一个跨平台的应用框架,诞生之后,就被高度关注。
  • 它通过自绘 UI ,解决了之前 RN 和 weex 等类RN技术方案难以解决的多端一致性问题。
  • Flutter 采用Dart语言开发、AOT编译(iOS AOT/安卓JIT,关于Flutter的编译方式可以参考这篇文章
    • DartAOT精减的渲染管线,相对与 JavaScript 和 webview 的组合,具备更高的性能体验。
  • 了解底层引擎的工作原理可以帮助我们更深入地结合具体的业务来对引擎进行定制和优化,更好的去创新和支撑业务。
  • 本文先对 Flutter 的底层渲染引擎做一下深入分析和整理,以理清 Flutter 的渲染的机制及思路,为后面进一步学习Flutter进行开发,理解背后的渲染原理做知识储备。

概述

  • Flutter 界面是由 Widget 组成的,所有 Widget 组成 Widget Tree,界面更新时会更新 Widget Tree,然后再更新 Element Tree,最后更新 RenderObject Tree
  • 接下来的渲染流程:
    • 在 Framework 层:
      • Flutter 渲染在 Framework 层会有 BuildWiget TreeElement TreeRenderObject TreeLayoutPaintComposited Layer 等几个阶段
    • 在 GPU 层:
      • 将 Layer 进行组合,生成纹理
      • 原生使用 OpenGL 的接口向 GPU 提交渲染内容进行光栅化与合成的过程,在 Flutter 中是在其 C++ 层 完成使用的是 Skia 库
  • 包括提交到 GPU 进程后,合成计算,显示屏幕的过程和 Native基本是类似的,因此性能也差不多

一、Flutter的架构(渲染框架)

从附图中我们可以看到: Flutter的框架分为Framework和Engine两层:

  • 应用是基于Framework层开发的:Flutter重写了UI框架从UI控件到渲染全部自己重新实现了。Framework负责渲染中的BuildLayoutPaint生成Layer等环节。
  • 不依赖 iOS、Android 平台的原生控件,
  • Engine层是C++实现的渲染引擎:Flutter依赖Engine(C++)层的Skia图形库与系统图形绘制相关接口,负责把Framework生成的Layer组合生成纹理,然后通过Open GL接口向GPU提交渲染数据

二、 Flutter的渲染过程

整个 Flutter 的 UI 生成以及渲染完成主要分下面几个步骤:

其中 1-6 在收到系统 vsync 信号后,在 UI 线程中执行,主要是涉及在 Dart framework 中 Widget/Element/RenderObject 三颗树的生成以及承载绘制指令的 LayerTree 的创建; 7-8 在 GPU 线程中执行,主要涉及光栅化合成上屏;

1-4跟渲染没有直接关系主要就是管理UI组件生命周期,页面结构以及Flex layout等相关实现,本文不作深入分析;
5-8为渲染相关流程,其中5-6在UI线程中执行,产物为包含了渲染指令的Layer tree在Dart层生成Layer,可以认为是整个渲染流程的前半部属于生产者角色;
7-8把dart层生成的Layer Tree,通过window透传到Flutter engine的C++代码中,通过flow模块来实现光栅化并合成输出。可以认为是整个渲染流程的后半部属于消费者角色

我们可以进一步将这个渲染流程分为5个阶段:

  1. 当需要更新UI的时候,Framework通知Engine

    • 简单来说,Flutter的界面由Widget组成。

    • 所有Widget会组成Widget Tree。

    • 界面更新时,会更新Widget Tree,

    • 再更新Element Tree,最后更新RenderObjectTree。

    • 更新Widget的逻辑如下:

  2. Engine会等到下个Vsync信号到达的时候,会通知Framework

  3. Framework会进行animations, build,layout,compositing,paint,最后生成layer提交给Engine

  4. Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU

    • Flutter 渲染在 Framework 层会有 Build、Widget Tree、Element Tree、RenderObject Tree、Layout、Paint、Composited Layer 等几个阶段

    • 在 Flutter 的 C++ 层,使用 Skia 库,将 Layer 进行组合,生成纹理,使用 OpenGL 的接口向 GPU 提交渲染内容进行光栅化与合成。

    • 提交到 GPU 进程后,合成计算,显示屏幕的过程和 iOS 原生渲染基本是类似的,因此性能上是差不多的。

  5. GPU经过处理后在显示器上面显示。整个流程如下图:

从流程图可以看出来,只有当有UI更新的才需要重新渲染,当然程序启动的是默认去渲染的。

三、渲染触发

接下来我们先分析一下当有UI需要更新的时候,是怎么样触发渲染,从应用到Framework,再到Engine这个过程是怎么样的。在Flutter开发应用的时候,当需要更新的UI的时候,需要调用一下setState方法,然后就可以实现了UI的更新,我们接下来分析一下该方法做哪些事情。

void _drawFrame() { //Engine回调Framework入口 
  _invoke(window.onDrawFrame, window._onDrawFrameZone);
}
  //初始化的时候把onDrawFrame设置为_handleDrawFrame
  void initInstances() {
    super.initInstances();
    _instance = this;
    ui.window.onBeginFrame = _handleBeginFrame;
    ui.window.onDrawFrame = _handleDrawFrame;
    SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  }
  
  void _handleDrawFrame() {
    if (_ignoreNextEngineDrawFrame) {
      _ignoreNextEngineDrawFrame = false;
      return;
    }
    handleDrawFrame();
  }
  void handleDrawFrame() {

   
      _schedulerPhase = SchedulerPhase.persistentCallbacks;//记录当前更新UI的状态
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    }
  }
  void initInstances() {
    ....
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }
 void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
  }
    void drawFrame() {
    ...
     if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement); //先重新build widget
      super.drawFrame();
      buildOwner.finalizeTree();
      
  }
    void drawFrame() { //这个方法完成Layout,CompositingBits,Paint,生成Layer和提交给Engine的工作
    assert(renderView != null);
    pipelineOwner.flushLayout(); 
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); //生成Layer并提交给Engine
    pipelineOwner.flushSemantics(); 
  }


从上面代码分析得知,从Engine回调,Framework会build,Layout,Paint,生成Layer等环节。接下来具体分析一下,这些环节是怎么实现的。

四、渲染过程细节

4.1 Build

在Flutter应用开发中,无状态的widget是通过StatelessWidget的build方法构建UI,有状态的widget是通过State的build方法构建UI。现在具体分析一下从setState调用后到调用自定义State的build的流程是怎样的(现在只分析有状态的widget渲染过程)。

//这是官方的demo
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

//这里就是构建UI,当调用setState后就会调用到这里,重新生成新的widget
  @override
  Widget build(BuildContext context) {
   
    return new Scaffold(
    ...
    );
  }
}

//从上面代码的分析到,在调用了setState后,最终会调用到buildScope来build
void buildScope(Element context, [VoidCallback callback]) {
    ...
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
       ...
         _dirtyElements[index].rebuild();
        index += 1;
      }
      for (Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
  }
    void rebuild() {
   ...
    if (!_active || !_dirty)
      return;
    performRebuild();
   
  }
  void performRebuild() {
    ...
     built = build();
    ...
  }
   Widget build() => widget.build(this); 

从上面可以看出,buildScope会遍历_dirtyElements,对每个在数组里面的每个element调用rebuild,最终就是调用到相应的widget的build方法。 其实当setState的时候会把相应的element添加到_dirtyElements数组里,并且element标识dirty状态。

4.2 Layout

在Flutter中应用中,是使用支持layout的widget来实现布局的,支持layout的wiget有Container,Padding,Align等等,强大又简易。在渲染流程中,在widget build后会进入layout环节,下面具体分析一下layout的实现,layout入口是flushLayout。

void flushLayout() {
 ...
 while (_nodesNeedingLayout.isNotEmpty) {
    final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
    _nodesNeedingLayout = <RenderObject>[];
    for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {//这里是按照在node tree中的深度顺序遍历_nodesNeedingLayout,RenderObject的markNeedsLayout方法会把自己添加到_nodesNeedingLayout
      if (node._needsLayout && node.owner == this)//对于需要layout的RenderObject进行layout
        node._layoutWithoutResize();
    }
  }
  ...
}
void _layoutWithoutResize() {
  ...
  performLayout(); //这个方法是计算layout的实现,不同layout widget有不同的实现
  markNeedsSemanticsUpdate();
    ...
  _needsLayout = false;
  markNeedsPaint();
}
//这里就是列出来RenderView的计算布局的实现方式,这个比较简单,就是读取配置里面的大小,然后调用child的layout,其他widget layout的计算布局的方式是非常繁琐复杂的,可以自行分析代码
void performLayout() {
    assert(_rootTransform != null);
    _size = configuration.size;
    assert(_size.isFinite);
    
    if (child != null)
      child.layout(new BoxConstraints.tight(_size));//调用child的layout

}

//这个方法parent调用child的layout的入口,parent会把限制传给child,child根据限制来layout
void layout(Constraints constraints, { bool parentUsesSize: false }) {
    ...
    RenderObject relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
     
      return;
    }
    _constraints = constraints;
    _relayoutBoundary = relayoutBoundary;
    
    if (sizedByParent) {
        performResize(); 
    }
    RenderObject debugPreviousActiveLayout;
    
    performLayout();//实际计算layout的实现
    markNeedsSemanticsUpdate();
    
    _needsLayout = false;
    markNeedsPaint();
}
void performResize() {
    ...
    size = constraints.biggest;
    
    switch (axis) {
      case Axis.vertical:
        offset.applyViewportDimension(size.height);
        break;
      case Axis.horizontal:
        offset.applyViewportDimension(size.width);
        break;
    }
}

//这是标记为layout为dirty,把自己添加到渲染管道(PipelineOwner)里面
void markNeedsLayout() {
    
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
          return true;
        }());
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }


从上面分析出来,layout的整个过程,首先是当RenderOjbect需要重新layout的时候,把自己添加到渲染管道里面,然后再触发渲染到了layout环节,先从渲染管道里面遍历找出需要渲染的RenderObject,然后调用performLayout进行计算layout,而且不同的对象实现不同的performLayout方法,计算layout的方式也不一样,然后再调用child 的layout入口,同时把parent的限制也传给child,child调用自己的performLayout。

4.3 Paint

当需要描绘自定义的图像的时候,可以通过继承CustomPainter,实现paint方法,然后在paint方法里面使用Flutter提供接口可以实现复杂的图像。 下面具体分析一下paint流程是怎么实现的。


//这是官方的paint demo
 class Sky extends CustomPainter {
  
  @override
  void paint(Canvas canvas, Size size) {
    var rect = Offset.zero & size;
    var gradient = new RadialGradient(
      center: const Alignment(0.7, -0.6),
      radius: 0.2,
      colors: [const Color(0xFFFFFF00), const Color(0xFF0099FF)],
      stops: [0.4, 1.0],
    );
    canvas.drawRect(
      rect,
      new Paint()..shader = gradient.createShader(rect),
    );
  }

  @override
  bool shouldRepaint(Sky oldDelegate) => false;
}
//这是在渲染管道中paint的入口
void flushPaint() {
  final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
  _nodesNeedingPaint = <RenderObject>[];
  // Sort the dirty nodes in reverse order (deepest first).
  for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) { //这是实现的方式和layout过程基本类似,不过排序是反序的
    assert(node._layer != null);
    if (node._needsPaint && node.owner == this) {
      if (node._layer.attached) {
        PaintingContext.repaintCompositedChild(node);
      } else {
        node._skippedPaintingOnLayer();
      }
    }

    }
}

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
    ...
    if (child._layer == null) {  
      child._layer = new OffsetLayer();
    } else {
      child._layer.removeAllChildren();
    }
    
    final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds); //通过layer生成 painting context
    child._paintWithContext(childContext, Offset.zero);
    childContext._stopRecordingIfNeeded();
}
  
void _paintWithContext(PaintingContext context, Offset offset) {
  ...
  paint(context, offset); 
 ...
}
void paint(PaintingContext context, Offset offset) {
    if (_painter != null) { //只有持有CustomPainter情况下,才继续往下调用自定义的CustomPainter的paint方法,把canvas传过去
      _paintWithPainter(context.canvas, offset, _painter);
      _setRasterCacheHints(context);
    }
    super.paint(context, offset); //调用父类的paint的方法
        if (_foregroundPainter != null) {
          _paintWithPainter(context.canvas, offset, _foregroundPainter);
          _setRasterCacheHints(context);
    }
}
//super paint 在父类的paint里面继续调用child的paint,实现父子遍历
void paint(PaintingContext context, Offset offset) {
    if (child != null){
      context.paintChild(child, offset); 
}



void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
    int debugPreviousCanvasSaveCount;
    canvas.save();
    if (offset != Offset.zero)
      canvas.translate(offset.dx, offset.dy);
    painter.paint(canvas, size);//,在调用paint的时候,经过一串的转换后,layer->PaintingContext->Canvas,最终paint就是描绘在Canvas上
    ...
    canvas.restore();
} 

总结来说,paint过程中,渲染管道中首先找出需要重绘的RenderObject,然后如果有实现了CustomPainter,就是调用CustomPainter paint方法,再去调用child的paint方法。

4.4 Composited Layer

    Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
    try {
      final ui.SceneBuilder builder = new ui.SceneBuilder();
      layer.addToScene(builder, Offset.zero);
      final ui.Scene scene = builder.build();
      ui.window.render(scene);
      scene.dispose();
      assert(() {
        if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
          debugCurrentRepaintColor = debugCurrentRepaintColor.withHue(debugCurrentRepaintColor.hue + 2.0);
        return true;
      }());
    } finally {
      Timeline.finishSync();
    }
  }
    void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
    addChildrenToScene(builder, offset + layerOffset);
  }
  void addChildrenToScene(ui.SceneBuilder builder, Offset childOffset) {
    Layer child = firstChild;
    while (child != null) {
      child.addToScene(builder, childOffset);
      child = child.nextSibling;
    }
  }


Composited Layer就是把所有layer组合成Scene,然后通过ui.window.render方法,把scene提交给Engine,到这一步,Framework向Engine提交数据基本完成了。Engine会把所有的layer根据大小,层级,透明度计算出最终的显示效果,通过Openg Gl接口渲染到屏幕上。

五、总结

本篇文章主要从渲染角度去了解Flutter,便于让我们了解Flutter这个跨平台UI SDK的可靠性。
通篇的整体阐述了Flutter框架结构Flutter渲染流水线等。我们也得出了一个结论:Flutter页面渲染性能 ≈ Native

本文并未讨论的要点大概如下:
  • Flutter如何学?
  • Dart语言学习
  • Flutter实战技术学习
  • Flutter底层技术、源码阅读
  • Flutter自定义插件开发与开源
  • ......

六、参考

推荐阅读

相关阅读(共计14篇文章)

iOS相关专题

webApp相关专题

跨平台开发方案相关专题

阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

Android、HarmonyOS页面渲染专题

小程序页面渲染专题

总结