当前位置: 首页 > article >正文

flutter 专题七 Flutter面试之渲染流程

一、 简介

Flutter面试中必问的一个面试题就是渲染相关的话题。作为Google在2018年发布的一款跨平台UI框架,使用Dart作为其开发语言,底层使用Skia图形库进行视图渲染,渲染速度和用户体验堪比原生。

二、Flutter渲染流程

总的来说,Flutter中一帧的渲染可以分为三个过程:请求渲染、绘制和光栅化。

三、请求渲染阶段

Flutter中也是通过调用setState方法来通知刷新UI。

  1. 调用setState方法,将需要刷新的RenderObject加入dirtyList中;
  2. 调用window对象scheduleFrame函数,scheduleFrame函数是一个native函数,Dart层只是一个函数声明,具体逻辑是在C++层实现;
  3. C++层的scheduleFrame函数会调用Animator对象进行RequestFrame,最终会通过JNI调用回到Java层,调用Android系统的Choreographer监听下一个Vsync信号。
    对应的源码如下:
void setState(VoidCallback fn) {
   ...
    _element.markNeedsBuild(); //通过相应的element来实现更新,关于element,widget,renderOjbect这里不展开讨论
  }

  void markNeedsBuild() {
   ...
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }

   void scheduleBuildFor(Element element) {
    ...
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled(); //这是一个callback,调用的方法是下面的_handleBuildScheduled
    }
    _dirtyElements.add(element); //把当前element添加到_dirtyElements数组里面,后面重新build会遍历这个数组
    element._inDirtyList = true;

  }
    void _handleBuildScheduled() {
    ...
    ensureVisualUpdate();
  }
    void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }
    void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
    ...
    ui.window.scheduleFrame();
    _hasScheduledFrame = true;
  }
    void scheduleFrame() native 'Window_scheduleFrame';//这个方法是Engine实现的,把接口暴露给Framework,调用这个方法通知引擎,需要更新UI,引擎会在下一个vSync的到达的时候通知Framework

四、绘制阶段

  1. Choreographer接受到下一个Vsync信号后,会执行之前注册回调函数,这里最终会调用到C++层Animator的DrawFrame函数;
  2. 在DrawFrame函数中会执行Dart环境中window对象的DrawFrame函数;
  3. 在window对象DrawFrame函数中调用flushLayout对RenderObjectTree进行测量和布局,然后调用flushPaint进行绘制,绘制结束后,会生成一颗LayerTree。
    涉及的代码如下所示。
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等环节。

五、Build

在Flutter应用开发中,无状态的widget是通过StatelessWidget的build方法构建UI,有状态的widget是通过State的build方法构建UI。比如:

//这是官方的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状态。

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

七、Paint

当需要描绘自定义的图像的时候,可以通过继承CustomPainter,实现paint方法,然后在paint方法里面使用Flutter提供接口可以实现复杂的图像。 后面我们会详细讲一下Paint流程的实现,此处不再过多赘述。

八、Composited Layer

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

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

九、光栅化阶段

  1. Dart层绘制结束后,会调用window的render方法将LayerTree同步到C++环境中,window的render也是一个native方法,具体的实现在C++层
  2. C++层的Render方法会调用Animator对象的Render方法,在Render方法中会将LayerTree加入到LayerTreePipeLine中,这是一个典型的生产者消费者模式。
  3. Dart UI线程负责将绘制好的LayerTree加入pipeline中,GPU线程负责从pipeline中获取LayerTree进行光栅化,pipeline中最多存在2个LayerTree。
  4. CPU线程从pipeline中获取到LayerTree后,会调用Rasterizer的Draw方法进行光栅化,这里的光栅化实际上是通过递归遍历LayerTree将每一个Layer通过SKCanvas绘制到FrameBuffer。
  5. FlutterActivity启动时会创建GLContext,也就是OpneGL的上下文环境,当光栅化需要创建SKCanvas时,会通过GLContext获取FBO(FrameBufferObject),然后通过FBO创建SKSurface,在通过SKSurface创建SKCanvas。
  6. 当LayerTree绘制结束后,会通过SwrapBuffer方法将数据提交给显示器。

十、完整流程

十一、Paint阶段

Flutter的绘制主要包括两步:第一步是在Dart遍历执行RenderObject的paint方法,完成不同RenderObejct的绘制,生成一个LayerTree。第二步是在C++层将这颗LayerTree最终渲染到屏幕。

首先,我们看一下,Dart层LayerTree的生成过程。

  1. 创建PictureLayer,添加到RootLayer,形成一个LayerTree,这里以只有两个节点的LayerTree为例
  2. 创建Dart层的Canvas对象,首先会在C++层创建SKPictureRecord,然后通过SKPictureRecord创建一个SKCanvas,Dart层的Canvas对象只是C++中SKCanvas对象的一个代理。Dart环境中在Canvas对象上所有绘制操作,都会被记录到SKPictureRecord里面
  3. 遍历RenderObjectTree,调用RenderObject对象的paint方法,不同的RenderObeject重写paint方法,通过Canvas对象,进行绘制自己。
  4. 遍历结束后,调用SKPictureRecord的endRecording方法,结束记录,并且生成一个SKPicture对象,在Dart层对应一个Picture对象。SKPicture可以看一做是一帧屏幕截图,源码中SKPicture也是可以直接转成Image。
  5. 将Dart层生成的Picture对象赋值给PictureLayer,这样Dart层的LayerTree创建完成。
  6. 将Dart层的LayerTree同步到C++,通过GPU线程进行光栅化处理。

接下来,是C++层渲染LayerTree过程。

  1. FlutterActivity启动时,会在C++层完成GL环境的初始化和GLConetxt的创建
  2. FlutterSurfaceView的Surface创建完成后会同步到C++,通过这个Surface在C++层创建AndroidEGLSurface,作为GL的目标渲染Surface
  3. 通过AndroidEGLSurface创建SKSurface,在这个SKSurface上绘制的东西都会代理到AndroidEGLSurface
  4. 通过SKPicture创建SKCanvas,然后遍历LayerTree,通过SKCanvas绘制所有的Layer节点,完成光栅化处理,最终将光栅化后的数据刷新到手机屏幕上完成渲染。


http://www.kler.cn/a/379703.html

相关文章:

  • 电子电气架构 --- 车载诊断的快速入门
  • 支持 Mermaid 语言预览,用通义灵码画流程图
  • C++和OpenGL实现3D游戏编程【连载17】——着色器进阶
  • 海睿思产品体系二次开发能力介绍
  • Zookeeper 简介 | 特点 | 数据存储
  • 【学术论文投稿】探索嵌入式硬件设计:揭秘智能设备的心脏
  • 硬件在环仿真建模之电路拓扑建模与数学建模
  • java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
  • oceanbase V4.2.2社区版集群离线部署
  • 图像边缘算法复现研究
  • 详解:枚举类
  • 【力扣专题栏】K个一组对链表进行翻转,如何实现分组翻转链表?
  • 东方通TongWeb替换Tomcat的踩坑记录
  • VLAN间通信以及ospf配置
  • 2024年第六届全球校园人工智能算法精英大赛——【算法挑战赛】钢材表面缺陷检测与分割 比赛复盘
  • aws(学习笔记第十课) 对AWS的EBS如何备份(snapshot)以及使用snapshot恢复数据,AWS实例存储
  • 深度学习-如何计算神经网络的输出?
  • 重学SpringBoot3-整合 Elasticsearch 8.x (二)使用Repository
  • 为什么说模拟电路的难点就在开通过程和关断过程?难在什么地方?
  • 【数学二】线性代数-矩阵-初等变换、初等矩阵
  • 数据结构模拟题[十一]
  • 【使用 Python 和 ADB 检查 Android 设备的 Wi-Fi 状态】
  • python实现钉钉群机器人消息通知(消息卡片)
  • kafka消费端常见故障及处理方法
  • MySQL 高性能优化规范建议
  • 浅谈RPC的实现原理与RPC实战