Flutter视图的Layout与Paint

通知:flutter最近 1.0 了!

本文目的

  • 分析flutter的Layout与Paint
  • relayout boundary和repaint boundary是什么
  • 开发者如何使用relayout boundary和repaint boundary

目录结构

  • Flutter的绘图原理和UI的基本流程
  • Widget在flutter绘图时的做用
  • 分析Layout
  • 分析Paint
  • 总结

Flutter的绘图原理和UI的基本流程

  • Flutter的绘图原理

flutter-vsync
从图中能够看到,当GPU发出vsync信号时,会执行Dart代码绘制新UI,Dart-code会被执行为Layer Tree,而后通过Compositor合成后交由Skia引擎渲染处理为GPU数据,最后经过GL/Vulkan发给GPU。 而咱们要分析的地方就在Dart->Layer Tree这里。

  • UI的基本流程

render-pipeline

好比用户一个输入操做,能够理解发出为Vsunc信号,这时,fliutter会先作Animation相关工做,而后Build当前UI,以后视图开始布局和绘制。生成视图数据,可是只会生成Layer Tree,并不能直接使用,仍是须要Composite合成为一个Layer进行Rasterize光栅化处理。层级合并的缘由是由于通常flutter的层级不少,直接把每一层传给GPU传递,效率很低,因此会先作Composite,提升效率。 光栅化以后才会给Flutter-Engine处理,这里只是Framework层面的工做,因此看不到Engine,而咱们分析的也只是Framework中的一小部分。html

flutter-pipeline
经过上面的讲解,咱们大概已经了解了flutter的绘图的基本流程,可是咱们并不清楚layout和paint作了什么,而Widget是如何变成Layou Tree的。可是这里内容太多,一句话说不清,因此咱们仍是先看下咱们平时写的大量Widget在flutter绘图时的究竟是啥用吧。

Widget在Flutter绘图时的做用

在这以前,咱们要先了解几个概念git

  • Widget
  • Element
  • RenderObject
Widget

这里的Widget就是咱们平时写的Widget,它是 Flutter中控件实现的基本单位。 一个Widget里面通常存储了视图的配置信息,包括布局、属性等等。因此它只是一份直接使用的数据结构。在构建为结构树,甚至从新建立和销毁结构树时都不存在明显的性能问题。github

Element

Element是Widget的抽象,它承载了视图构建的上下文数据。flutter系统经过遍历 Element树来构建 RenderObject数据,因此Element是真正被使用的集合,Widget只是数据结构。好比视图更新时,只会标记dirty Element,而不会标记dirty Widget。canvas

RenderObject

咱们要分析的Layout、Paint均发生在RenderObject中,而且LayerTree也是由RenderObject生成,可见其重要程度。因此 Flutter中大部分的绘图性能优化发生在这里。RenderObject树构建的数据会被加入到 Engine所需的 LayerTree中。segmentfault

Widget-Element-RenderObject
而以上这三个概念也对应着三种树结构:模型树、呈现树、渲染树。 在解释他们的概念和关系之后,咱们已经认识到RenderObject的重要性,由于如下Layout、Paint包括relayout boundary和repaint boundary都是在这里发生的。 通常一个Widget被更新,那么持有该Widget的节点的Element会被标记为dirtyElement,在下一次更新界面时,Element树的这一部分子树便会被触发performRebuild,在Element树更新完成后,便能得到RenderObject树,接下来会进入Layout和Paint的流程。

Layout

  • Layout的目的是要计算出每一个节点所占空间的真实大小。

layout-data-flow
在构建视图树的时候,节点的Constraints是自上而下的,可是计算layout是深度优先遍历,这是由于节点经过Constraints并不必定可以明确本身的size,有时它会依赖子节点的size,因此获取size大小是自下而上。 每一个节点会接受到父对象的Constraints,子节点根据其来决定本身的大小,父对象会根据本身的逻辑决定子对象的位置来完成布局。 因此flutter的layout实际上就是这么简单的操做。那么简单确定就有一些问题,好比某个节点的size变了,整个视图树就得从新计算? 确定不是这样的,不然flutter就不存在图形的高性能了。flutter是经过Relayout boundary来处理这样的问题的。

  • Relayout boundary

它的目的是提升flutter的绘图性能,它的做用是设置测量边界,边界内的Widget作任何改变都不会致使边界外从新计算并绘制。性能优化

Relayout boundary
固然它是有条件的,当知足如下三个条件的任意一个就会触发Relayout boundary

  • constraints.isTight
  • parentUsesSize == false
  • sizedByParent == true
constraints.isTight

什么是isTight呢?用BoxConstraints为例bash

BoxConstraints
它有四个属性,分别是minWidth,maxWidth,minHeight,maxHeight

tight 若是最小约束(minWidth,minHeight)和最大约束(maxWidth,maxHeight)分别都是同样的数据结构

loose 若是最小约束都是0.0(无论最大约束),若是最小约束和最大约束都是0.0,就同时是tightly和looseless

bounded 若是最大约束都不是infiniteide

unbounded 若是最大约束都是infinite

expanding 若是最小约束和最大约束都是infinite

因此isTight就是强约束,Widget的size已经被肯定,里面的子Widget作任何变化,size都不会变。那么从该Widget开始里面的任意子Wisget作任意变化,都不会对外有影响,就会被添加Relayout boundary(说添加不科学,由于实际上这种状况,它会把size指向本身,这样就不会再向上递归而引发父Widget的Layout了)

parentUsesSize == false

实际上parentUsesSize与sizedByParent看起来很像,但含义有很大区别 parentUsesSize表示父Widget是否要依赖子Widget的size,若是是false,子Widget要从新布局的时候并不须要通知parent,布局的边界就是自身了。

sizedByParent == true

sizedByParent表示当前的Widget虽然不是isTight,可是经过其余约束属性,也能够明确的知道size,好比Expanded,并不必定须要明确的size。

经过查看RenderObject-1579行,固然能够看到Layout的实现

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ...省略1w+...
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    ...省略1w+...
}
复制代码

经过Layout能够看到,flutter为了提升效率所作的努力,那做为开发者能够直接使用relayout boundary吗?通常状况是不能够的,可是若是当你决定要自定义一个Row的时候,确定是要使用它的。可是你能够间接的利用上面的三个条件来使你的Widget树某些地方拥有relayout boundary。好比如下用法

Row(children: <Widget>[
        Expanded(child: Container(
                      height: 50.0, // add for test relayoutBoundary
                      child: LayoutBoundary(),
                 )),
        Expanded(child: Text('You have pushed the button this many times:'))
]
复制代码

若是你想测试上面的三个条件成立时是否真的不会再layout,你能够自定义LayoutBoundaryDelegate来测试,好比

class LayoutBoundaryDelegate extends MultiChildLayoutDelegate {
  LayoutBoundaryDelegate();

  static const String title = 'title';
  static const String summary = 'summary';
  static const String paintBoundary = 'paintBoundary';

  @override
  void performLayout(Size size) {
    print('TestLayoutDelegate performLayout ');

    final BoxConstraints constraints = BoxConstraints(maxWidth: size.width);

    final Size titleSize = layoutChild(title, constraints);
    positionChild(title, Offset(0.0, 0.0));

    final double summaryY = titleSize.height;
    final Size descriptionSize = layoutChild(summary, constraints);
    positionChild(summary, Offset(0.0, summaryY));

    final double paintBoundaryY = summaryY + descriptionSize.height;
    final Size paintBoundarySize = layoutChild(paintBoundary, constraints);
    positionChild(
        paintBoundary, Offset(paintBoundarySize.width / 2, paintBoundaryY));
  }

  @override
  bool shouldRelayout(LayoutBoundaryDelegate oldDelegate) => false;
}
复制代码

自定义的MultiChildLayoutDelegate须要使用CustomMultiChildLayout来配合使用

Container(
          child: CustomMultiChildLayout(
              delegate: LayoutBoundaryDelegate(),
              children: <Widget>[
                LayoutId(
                    id: LayoutBoundaryDelegate.title,
                    child: Row(children: <Widget>[
                      Expanded(child: LayoutBoundary()),
                      Expanded(child: Text( 'You have pushed the button this many times:'))
                 ])),
                LayoutId(
                    id: LayoutBoundaryDelegate.summary,
                    child: Container(
                        child: InkWell(
                          child: Text(
                           _buttonText,
                           style: Theme.of(context).textTheme.display1),
                        onTap: () {
                          setState(() {
                            _index++;
                            _buttonText = 'onTap$_index';
                          });
                        },
                      ))),
                LayoutId(
                    id: LayoutBoundaryDelegate.paintBoundary,
                    child: Container(
                      width: 50.0,
                      height: 50.0,
                      child: PaintBoundary())),
              ]),
        )
复制代码

咱们在performLayout方法里作了打印操做,若是CustomMultiChildLayout的children里的任意一个child的size变化,就会打印这条信息,因此这样的代码在每次点击onTap的时候,都会打印'TestLayoutDelegate performLayout'

print-relayout
因此为了达到有RelayoutBoundary的效果,能够将代码中的Container添加宽高以达到constraints.isTight条件,这个实验就留给读者本身测试吧。

Paint

paint的一个重要工做就是肯定哪些Element放在同一Layer

paint-into-layers
布局size计算是自下而上的,可是paint是自上而下的。在layout以后,全部的Widget的大小、位置都已经肯定,这时不须要再作遍历。

paint-target-layer-flow
Paint也是按照深度优先的顺序,并且老是先绘制自身,再是子节点,好比节点 2是一个背景色绿色的视图,在绘制完自身后,绘制子节点3和4。当绘制完之后,Layer是按照深度优先的倒叙进行返回,相似Size的计算,而每一个Layer就是一层,最后的结果是一个Layer Tree。 也许你已注意到在2节点因为一些其余缘由致使它的部分UI5与6处于了同一层,这样的结果会致使当2须要重绘的时候,与其不想相关的6实际上也会被重绘,而存在性能损耗。Flutter的工程师固然不会做出这么愚蠢的设计。因此为了提升性能,与relayout boundary相应的存在repaint boundary。

  • repaint boundary 若是发生上面状况,repaint boundary会强制的使2切换到新Layer

repaint boundary
这样强制使图层分开,以达到绝不相关的控件的Paint的时候,不会被影响致使重绘。 Repaint boundary通常不须要开发者设置。但开发者能够手动设置,Flutter提供RepaintBoundary组件,你能够在你认为须要的地方,设置Repaint boundary。 如何验证添加RepaintBoundary后,child就不会被同层的Widget的repaint影响呢,咱们能够自定义一个Paint,好比

class PaintBoundary extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(painter: CirclePainter(color: Colors.orange));
  }
}

class CirclePainter extends CustomPainter {
  final Color color;

  const CirclePainter({this.color});

  @override
  void paint(Canvas canvas, Size size) {
    print('CirclePainter paint');
    var radius = size.width / 2;
    var paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;
    canvas.drawCircle(Offset(radius, size.height), radius, paint);
  }

 @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
复制代码

只是很简单的绘制一个橙色的圆,在RelayoutBoundary验证代码中已贴出使用。咱们只需看设置RepaintBoundary和不设置时候的区别。实验验证结果RelayoutBoundary确实能够避免CirclePainter发生重绘,即'CirclePainter paint'只会打印一次。 读者能够本身尝试验证。

总结

relayout boundary和repaint boundary都是Flutter为了提升绘图性能而作的努力。 一般开发者可使用RepaintBoundary组件来提升应用的性能,也能够根据relayout boundary的几个规则来使relayout boundary生效,从而提升性能。

[测试代码传送门](http://link.zhihu.com/?target=https%3A//github.com/Dpuntu/RePaintBoundary-RelayoutBoundary)

参考

本文版权属于再惠研发团队,欢迎转载,转载请保留出处。@Dpuntu

相关文章
相关标签/搜索