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

相關文章
相關標籤/搜索