资讯专栏INFORMATION COLUMN

Flutter中的布局绘制流程简析(二)

icattlecoder / 962人阅读

摘要:所以这里为时把指向自身,因为自身的肯定符合约束的条件,也是提高布局效率的一个关键点。举一个栗子,在中先让布局之后,根据的,来设置自身的。意味着父控件要依赖子控件的,可能父控件的布局要根据子控件的来做调整。

布局约束

刚才所说的改变一个控件的高度,有时候并不像刚才所说只是改变一下属性就能起作用,这里涉及到一个布局约束规则。
直接看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.
"
        "$violation
"
        "$hint
"
        "$contract
"
        "The RenderBox in question is:
"
        "  $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成一帧画面。
总结之前的,现在我们可以得出以下一张的关系图:

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

这种关系不言而喻,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

经过_updateCompositingBits处理后:

而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 dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = [];
      // 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方法去做最后真实渲染,之后就是底层引擎的工作内容,有机会再去更加深入去学习吧。

结束

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89627.html

相关文章

  • Flutter中的布局绘制流程简析(一)

    摘要:入口界面的布局和绘制在每一帧都在发生着,甚至界面没有变化,它也会存在可以想象每一帧里面,引擎都像流水线的一样重复着几个过程构建控件树,布局绘制和合成,周而复始。大概可以想到的主要功能负责管理那些,让它们进行布局和绘制。 开始 Flutter对比前端流行的框架,除了构建控件树和控件状态管理等,还多了布局和绘制的流程,布局和绘制以往都是前端开发可望而不可及的都被封锁在浏览器渲染引擎的实现里...

    duan199226 评论0 收藏0
  • Flutter之SchedulerBinding简析

    摘要:但是接下来并不是讨论单线程如何方便开发,而是要深入的调度器,看一下是如何安排任务,调度工作。总结在大部分情况下,其实并不用担心会像游戏一样疯狂消耗电量,消耗电量表现应该跟原生没有多大差别。 开始 在原生开发中(例如Android)都会强调不能阻塞主线程,但是开发中经常会遇到发送请求或者操作数据库等,这些操作都会阻塞主线程,几乎唯一办法就是用多线程处理这些工作;而在Flutter中就像跟...

    BlackMass 评论0 收藏0
  • Flutter样式和布局控件简析()

    摘要:开始继续接着分析相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深的体会。关于属性,指前一个组件的布局区域和绘制区域重叠了。 开始 继续接着分析Flutter相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深...

    yck 评论0 收藏0
  • Flutter样式和布局控件简析()

    摘要:开始继续接着分析相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深的体会。关于属性,指前一个组件的布局区域和绘制区域重叠了。 开始 继续接着分析Flutter相关的样式和布局控件,但是这次内容难度感觉比较高,怕有分析不到位的地方,所以这次仅仅当做一个参考,大家最好可以自己阅读一下代码,应该会有更深...

    leanxi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<