阅读 23

Flutter 探索系列:布局和渲染(二)

上一篇文章中,我们介绍 Flutter Widget 的设计思想、实现原理,并分析了 Widget、Element 和 RenderObject 的源码,这篇文章继续结合源码分析 Flutter 的渲染过程。

实现原理

1,Flutter 渲染流程是怎样的?

从这张图上可知,界面显示到屏幕上,Flutter 经过了 Vsync 信号、动画、build、布局、绘制、合成等渲染过程。

显示器垂直同步 Vsync 不断的发出信号,驱动 Flutter Engine 刷新视图。Flutter 提供 60 FPS,也就是一秒钟发出60次信号,触发60次重绘。

运行动画,每个 Vsync 信号都会触发动画状态改变。

Widget 状态改变,触发重建一棵新的 Widget 树。比较新旧 Widget 树的差异,找出有变动的 Widget 节点和对应的 RenderObject 节点。详细过程请参考上一篇文章。

对需要更新 RenderObject 节点,更新界面布局、重新绘制。

根据新的 RenderObject 树,更新合成图层。

输出新的图层树。

2,渲染过程中,Flutter 如何更新界面布局? 经过 build 环节后,找出需要更新的 RenderObject 树,首先进入布局环节。上一篇文章中介绍到,element 被标记为 dirty 时便会重建,同样的,RenderObject 被标记为 dirty 后,放入一个待处理的数组中。在布局环节中,遍历该数组中的元素,按照节点在 RenderObject 树上的深度重新布局。

每个节点 RenderObject 对象,按照部件的逻辑先计算自身所占空间的大小、位置,再计算 child 的,paren 将 size 约束限制传递给 child,child 根据这个约束计算自身的所占空间,然后再传给 child 的 child,如此递归完成整个 RenderObject 树的布局更新。大概过程如下

parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....
复制代码

Flutter 还有一个 RelayoutBoundary,用于确定重绘边界,可以手动指定或自动设置。边界内的对象重新布局,不会影响边界外的对象。

3,渲染过程中,Flutter 如何绘制界面? Paint 的过程有点类似于 Layout,同样将待重新绘制的 RenderObject 标记为 dirty,放入一个数组中。这个数组也是深度优先的顺序执行,先绘制自身,再绘制 child。

isRepaintBoundary 重绘边界也类似上面的 RelayoutBoundary,重绘边界内的元素及 child,会一起重新绘制,边界外的元素不受影响。

源码分析

我们按照 Flutter 的渲染依次分析 Vsync、build、layout、paint 四个过程 。

Vsync

垂直同步信号 Vsync 到来后,执行一系列动作开始界面的重新渲染,那么在哪里监听 Vsync 信号,收到 Vsync 信号如何通知界面刷新?

上一篇文章介绍了 Widget build 实现过程,其中提到在 Flutter 应用启动时,初始化了一个单例对象 WidgetsFlutterBinding,它是连接 Flutter engine sdk 和 Widget 框架的桥梁,它混合了 SchedulerBinding 和 RendererBinding。SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件。

RendererBinding 在初始化方法 initInstances 中,addPersistentFrameCallback 向 persistentCallbacks 队列添加了一个回调 _handlePersistentFrameCallback。

在收到从 Flutter engine 传来的刷新事件时,调用 _handlePersistentFrameCallback 回调,也就是执行 drawFrame 方法。

// RendererBinding
void initInstances() {
    ...
    addPersistentFrameCallback(_handlePersistentFrameCallback);
}
  
void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
}

// SchedulerBinding
void addPersistentFrameCallback(FrameCallback callback) {
    _persistentCallbacks.add(callback);
}
复制代码

那么 persistentCallbacks 队列什么时候被执行?

这里先介绍一个类 Window,它是 Flutter engine 提供的一个图形界面相关的接口,包括了屏幕尺寸、调度接口、输入事件回调、图形绘制接口和其他一些核心服务。Window 有一个绘制的回调方法 _onDrawFrame

当 Flutter engine 调用 _onDrawFrame 时,触发 SchedulerBinding.handleDrawFrame 方法,这个方法里面遍历执行已注册的回调,即前面注册的 drawFrame 方法。

// SchedulerBinding
  void ensureFrameCallbacksRegistered() {
    window.onBeginFrame ??= _handleBeginFrame;
    window.onDrawFrame ??= _handleDrawFrame;
  }
  
void handleDrawFrame() {
    ...
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    ...
}
复制代码

Vsync 信号到来,Flutter engine 调用 _onDrawFrame 方法启动渲染流程,开启一系列的动作。

Build

收到刷新事件后,先调用 WidgetsBinding.drawFame 方法。这个方法重建 Widget 树,这一过程上篇文章有详细介绍,这里不多做赘述。

//WidgetsBinding
void drawFrame() {
    ...
    if (renderViewElement != null)
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    ...
}
复制代码

Layout

super.drawFrame() 会进入到 RenderBinding.drawFrame 方法,开始重新布局和绘制。

//RenderBinding
void drawFrame() {
  pipelineOwner.flushLayout(); //布局
  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
  pipelineOwner.flushPaint(); // 重绘
  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
  pipelineOwner.flushSemantics(); 
}
复制代码

flushLayout 方法内,遍历 _nodesNeedingLayout 数组,_nodesNeedingLayout 内存放的是被标记为 dirty 的 RenderObject 元素。遍历前先对 _nodesNeedingLayout 数组排序,按照深度优先的顺序重新排列,即先处理上层节点再处理下层节点,然后遍历每个元素重新布局。

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)) {
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    ...
}
复制代码

_layoutWithoutResize 调用 performLayout 方法,每个 RenderObject 子类都有不同的实现,以 RenderView 为例,它读取配置中的 size,然后调用 child 的 layout 方法,并把 size 限制传进去。同时将自身的布局标志 _needsLayout 设置为 false

void _layoutWithoutResize() {
   ...
   performLayout(); 
   markNeedsSemanticsUpdate();
   ...
   _needsLayout = false;
   markNeedsPaint();
}

void performLayout() {
    _size = configuration.size;
    
    if (child != null)
      child.layout(new BoxConstraints.tight(_size));//调用child的layout
}
复制代码

layout 方法中,传入的两个参数:constraints 表示 parent 对 child 的大小限制,parentUsesSize 表示 child 布局变化是否影响 parent,如果为 true,当 child 布局变化时,parent 会被标记为需要重新布局。

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(); 
    }
    
    performLayout();
    ...
}
复制代码

sizedByParent 表示 child size 完成由 parent 决定,所以当 sizedByParent 为 true 时,child size 在 performResize 中确定。当 sizedByParent 为 false 时,执行 performLayout 计算自身 size,并调用自身的 child 布局,最终调用链就变成:

parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....
复制代码

RelayoutBoundary,用于确定重绘边界。边界内的对象重新布局,不会影响边界外的对象。在 RenderObject 的 markNeedsLayout 方法中,从自身开始向 parent 查找到 relayoutBoundary,然后把它添加到待布局 _nodesNeedingLayout 数组中,等下次 Vsnc 信号到来时重新布局。

void markNeedsLayout() {
  ...
  if (_relayoutBoundary != this) {
    markParentNeedsLayout();
  } else {
    _needsLayout = true;
    if (owner != null) {
      ...
      owner._nodesNeedingLayout.add(this);
      owner.requestVisualUpdate();
    }
  }
}

  void markParentNeedsLayout() {
    _needsLayout = true;
    final RenderObject parent = this.parent;
    if (!_doingThisLayoutWithCallback) {
      parent.markNeedsLayout();
    } else {
      assert(parent._debugDoingThisLayout);
    }
  }
复制代码

Paint

布局完成后开始绘制,绘制的入口是 flushPaint。类似于布局,将需要重新绘制的 RenderObject 标记为 dirty,同样按照深度优先的顺序遍历 _nodesNeedingPaint 数组,每个元素都重新绘制。

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)) {
    if (node._needsPaint && node.owner == this) {
      if (node._layer.attached) {
        PaintingContext.repaintCompositedChild(node);
      } else {
        node._skippedPaintingOnLayer();
      }
    }
  }
  ...
}
复制代码

paint 由具体的 RenderObject 类重写,每个实现都不一样。如果 RenderObject 有 child,执行自身的 paint 后,再执行 paintChild,调用链: paint() -> paintChild() -> paint() ...

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
    ...
    OffsetLayer childLayer = child._layer;
    if (childLayer == null) {  
      child._layer = childLayer = OffsetLayer();
    } else {
      childLayer.removeAllChildren();
    }
    
    final PaintingContext childContext = PaintingContext(child._layer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);
    childContext._stopRecordingIfNeeded();
}

void _paintWithContext(PaintingContext context, Offset offset) {
  ...
  paint(context, offset); 
  ...
}


  void paintChild(RenderObject child, Offset offset) {
    ...
    if (child.isRepaintBoundary) {
      stopRecordingIfNeeded();
      _compositeChild(child, offset);
    } else {
      child._paintWithContext(this, offset);
    }
    ...
 }
复制代码

isRepaintBoundary 类似于上面布局中的 RepaintBoundary,它决定是否自身是否独立绘制,如果为 true,则独立绘制,否则随 parent 一块绘制。

最后将所有layer组合成Scene,然后通过 ui.window.render 方法,把 scene 提交给Flutter Engine

void compositeFrame() {
  ...
  try {
    final ui.SceneBuilder builder = ui.SceneBuilder();
    final ui.Scene scene = layer.buildScene(builder);
    if (automaticSystemUiAdjustment)
      _updateSystemChrome();
    ui.window.render(scene);
    scene.dispose(); 
  } finally {
    Timeline.finishSync();
  }
}
复制代码

参考资料

Flutter Flutter框架分析(四)-- Flutter框架的运行 Flutter运行机制-从启动到显示