3

布局约束

刚才所说的改变一个控件的高度,有时候并不像刚才所说只是改变一下属性就能起作用,这里涉及到一个布局约束规则。
直接看BoxConstraints的实现,这个类主要定义了minWidth和maxWidth,minHeight和maxHeight这些约束条件,child布局的时候可以根据parent给予的这些条件进行对应的布局。
简单介绍相关的一些术语:

  • tightly,如果最小约束(minWidth)和最大约束(maxWidth)都是一样的
  • loose,如果最小约束是0.0(不管最大约束);如果最小约束和最大约束都是0.0,就同时是tightly和loose
  • bounded,如果最大约束不是infinite
  • unbounded,如果最大约束是infinite
  • expanding,如果最小约束和最大约束都是infinite

如果一个size满足BoxConstraints的约束,那么它就是constrained的。

既然是parent传递给child的约束条件,当然是在performLayout的时候调起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) {
      try {
        performResize();
      } catch (e, stack) {
        _debugReportException('performResize', e, stack);
      }
    }
    RenderObject debugPreviousActiveLayout;
    try {
      performLayout();
      markNeedsSemanticsUpdate();
      assert(() { debugAssertDoesMeetConstraints(); return true; }());
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    _needsLayout = false;
    markNeedsPaint();
  }

开始分析这段代码前一小半,决定relayoutBoundary的值也就是布局边界,一个RenderObject想要重新布局,应该从哪里开始。
parentUsesSize,如果为false也就是,parent的布局并不需要依赖child的布局结果,那么child如果要重新布局并不需要通知parent,布局的边界就是自身了,而parentUsesSize的默认值也是为false,也就是大部分时候也只需自身重新布局。
parentUsesSize,如果为true就是parent的布局要依赖child布局(或者parent的size依赖于child的size,想象一下两个div嵌套的情况),再看如果sizedByParent和constraints.isTight都为false,在这种情况之下relayoutBoundary要指向parent.relayoutBoundary,也就是说child如果要重新布局,必须从relayoutBoundary开始,在RenderObject.markNeedsLayout方法实现里面,最终只会把relayoutBoundary加入到_nodesNeedingLayout列表中,跟isRepaintBoundary处理是几乎一样的;
但是如果constraints.isTight为true,也就是minWidth和maxWidth(或者minHeight和maxHeight)值都一样child的size没有变化的空间,只能在限定死的约束空间中布局,这个时候relayoutBoundary也是指向自身。
最后就是sizedByParent这个属性,说实在看名字不太明白它的意图,但是sizedByParent却决定performResize这个方法会不会调起,但是看了RenderBox.size的setter方法:

set size(Size value) {
    assert(!(debugDoingThisResize && debugDoingThisLayout));
    assert(sizedByParent || !debugDoingThisResize);
    assert(() {
      if ((sizedByParent && debugDoingThisResize) ||
          (!sizedByParent && debugDoingThisLayout))
        return true;
      assert(!debugDoingThisResize);
      String contract, violation, hint;
      if (debugDoingThisLayout) {
        assert(sizedByParent);
        violation = 'It appears that the size setter was called from performLayout().';
        hint = '';
      } else {
        violation = 'The size setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).';
        if (owner != null && owner.debugDoingLayout)
          hint = 'Only the object itself can set its size. It is a contract violation for other objects to set it.';
      }
      if (sizedByParent)
        contract = 'Because this RenderBox has sizedByParent set to true, it must set its size in performResize().';
      else
        contract = 'Because this RenderBox has sizedByParent set to false, it must set its size in performLayout().';
      throw new FlutterError(
        'RenderBox size setter called incorrectly.\n'
        '$violation\n'
        '$hint\n'
        '$contract\n'
        'The RenderBox in question is:\n'
        '  $this'
      );
    }());
    assert(() {
      value = debugAdoptSize(value);
      return true;
    }());
    _size = value;
    assert(() { debugAssertDoesMeetConstraints(); return true; }());
  }

大致可以总结一下,一般情况下都是都是根据parent给予的约束条件来计算size,而设置size只能在performResize或者performLayout中进行,如果设置sizedByParent为true,则只能在performResize中进行,否则就只能在performLayout中与child的布局同时进行。所以sizedByParent为true也意味着这个RenderObject的size不需要依赖于child的size,完全可以根据parent给予的约束条件可以确定(取最大或者最小的宽度和高度或者根据其他算法);但是为false,自身的size就要在performLayout决定,可能要在child的size和约束条件中计算出来,应该是更为复杂,根据注释sizedByParent默认为false永远都不会有问题的。所以这里sizedByParent为true时把relayoutBoundary指向自身,因为自身的size肯定符合约束的条件,也是提高布局效率的一个关键点。
举一个栗子,在RenderPadding中:

void performLayout() {
    _resolve();
    assert(_resolvedPadding != null);
    if (child == null) {
      size = constraints.constrain(new Size(
        _resolvedPadding.left + _resolvedPadding.right,
        _resolvedPadding.top + _resolvedPadding.bottom
      ));
      return;
    }
    final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
    child.layout(innerConstraints, parentUsesSize: true);
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = new Offset(_resolvedPadding.left, _resolvedPadding.top);
    size = constraints.constrain(new Size(
      _resolvedPadding.left + child.size.width + _resolvedPadding.right,
      _resolvedPadding.top + child.size.height + _resolvedPadding.bottom
    ));
  }

RenderPadding先让child布局之后,根据child的size,来设置自身的size。这里还涉及到一个Offset的问题,因为layout只是获取了size,但是元素在哪里开始绘制,一般也是由parent控制,当parent设置好每个child的offset之后在绘制的过程中就可以在适当的位置中绘制了。

parentUsesSize & sizedByParent

个人觉得这两个名称是最令人迷惑,所以这里再总结一下:

  • sizedByParent 顾名思义控件的大小完全在父控件的约束条件下,例如约束条件maxWidth=100,minWidth=0就意味着子控件的宽度只能在0到100的范围内。
  • parentUsesSize 意味着父控件要依赖子控件的size,可能父控件的布局要根据子控件的size来做调整。

还有这两者是否是冲突的尼,能否都为true?

根据我的分析这两个应该是不会造成冲突的,在选定布局的边界情况下,刚才代码中:

if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
   relayoutBoundary = this;
} else {
   final RenderObject parent = this.parent;
   relayoutBoundary = parent._relayoutBoundary;
}

如果parentUsesSize为true,布局边界毫无疑问指向了parent,也就是子控件要重新布局必须先从父控件开始,因为父控件需要用到子控件重新布局后的结果,所以在选定布局边界的问题上parentUsesSize起到决定性的作用。
如果sizedByParent和parentUsesSize都为true,例如父控件把maxWidth=100,minWidth=0这样的约束条件传递给子控件,意味着子控件布局的范围只能在0到100之间,假设子控件最终的size为50,父控件可能直接会使用子控件的size作为自身的size,这样一看好像现在父控件的布局范围现在应该是0到50,但其实是并不会影响一开始传递给子控件的约束条件0到100之间,所以不会触发一个循环布局。

Layer

继续PipelineOwner处理流程,在flushLayout之后就是flushCompositingBits方法,而flushCompositingBits目标是为每个RenderObject设置适当needCompositing值,这影响flutter最终会生成多少层Layer,而这些Layer会组成一棵Layer Tree并交由引擎最终composite成一帧画面。
总结之前的,现在我们可以得出以下一张的关系图:

clipboard.png

再对比一下之前的Chromium文档里面的一张图:
clipboard.png

这种关系不言而喻,Flutter确实就是一个super webview。

继续flushCompositingBits方法的深入:

void flushCompositingBits() {
    Timeline.startSync('Compositing bits');
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    Timeline.finishSync();
  }

跟之前flushLayout差不多,那么啥时候RenderObject会加入到PipelineOwner._needsCompositingBitsUpdate列表上尼?
扫了一下代码,发现除了框架初始化以外,一般都是在添加child和删除child的时候,调起RenderObject.markNeedsCompositingBitsUpdate方法。
继续_updateCompositingBits方法:

void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
  }

举个栗子,最初可能是这样的,RenderObject添加一个新的child,而这个child是被设置为alwaysNeedsCompositing

clipboard.png

经过_updateCompositingBits处理后:

clipboard.png

而needsComposting这个属性会用在那里尼?同样扫一遍代码,都可以发现类似的处理:

 if (needsCompositing) {
      pushLayer(new ClipRectLayer(clipRect: offsetClipRect), painter, offset, childPaintBounds: offsetClipRect);
    } else {
      canvas
        ..save()
        ..clipRect(offsetClipRect);
      painter(this, offset);
      canvas
        ..restore();
    }

如果needsCompositing为true,都会创建一个新的Layer,所以needsCompositing更多像一个暗示的作用,在clip,transform或者设置opacity都会创建一个Layer来处理,这样可以把一些经常变化的区域隔离开来,每次只需要绘制这部分区域来提高效率。

接着flushPaint方法:

void flushPaint() {
    Timeline.startSync('Paint', arguments: timelineWhitelistArguments);
    try {
      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();
          }
        }
      }
    } finally {
      Timeline.finishSync();
    }
  }

在repaintCompositedChild方法里面,会为RenderObject创建一个属于自己的Layer,其实也只限于isRepaintBoundary为true的RenderObject,因为只有这样的RenderObject才可以加入到_nodesNeedingPaint列表中:

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);
    child._paintWithContext(childContext, Offset.zero);
    childContext._stopRecordingIfNeeded();
  }

接着就是创建PaintingContext,就像前端需要获取Canvas2D Context一样,_paintWithContext最终会调起RenderObject.paint方法,在paint方法里面我们就可以自由绘制了,但是一般情况下CustomPaint组件就可以满足我们的需求。
再看一下PaintingContext类中:

Canvas get canvas {
    if (_canvas == null)
      _startRecording();
    return _canvas;
  }

  void _startRecording() {
    _currentLayer = new PictureLayer(canvasBounds);
    _recorder = new ui.PictureRecorder();
    _canvas = new Canvas(_recorder, canvasBounds);
    _containerLayer.append(_currentLayer);
  }

  void _stopRecordingIfNeeded() {
    if (!_isRecording)
      return;
    _currentLayer.picture = _recorder.endRecording();
    _currentLayer = null;
    _recorder = null;
    _canvas = null;
  }

当我们获取canvas做绘制操作的时候,每次都会创建一个新的Canvas对象,并使用使用ui.PictureRecorder记录我们在canvas的操作,最后_stopRecordingIfNeeded会从recorder上获取到绘制的picture,感觉这里有涉及到底层解析得不太好。。。

好吧,当flushPaint完成后,Layer Tree也构建出来了,最后就是composite阶段了,回到RenderView.compositeFrame方法:

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();
    } finally {
      Timeline.finishSync();
    }
  }

Flutter会把所有的Layer都加入到ui.SceneBuilder对象中,然后ui.SceneBuilder会构建出ui.Scene(场景),交给ui.window.render方法去做最后真实渲染,之后就是底层引擎的工作内容,有机会再去更加深入去学习吧。

结束

大致把整个布局渲染流程梳理一遍,感觉越到后面越吃力,有点超出认知的范围,也证明自己知识面仍然有很多不足的地方,如果有什么错漏的地方希望大家能够指正。


tain335
576 声望196 粉丝

Keep it simple.