通知:flutter最近 1.0 了!
本文目的
- 分析flutter的Layout與Paint
- relayout boundary和repaint boundary是什麼
- 開發者如何使用relayout boundary和repaint boundary
目錄結構
- Flutter的繪圖原理和UI的基本流程
- Widget在flutter繪圖時的作用
- 分析Layout
- 分析Paint
- 總結
Flutter的繪圖原理和UI的基本流程
- Flutter的繪圖原理
從圖中可以看到,當GPU發出vsync訊號時,會執行Dart程式碼繪製新UI,Dart-code會被執行為Layer Tree,然後經過Compositor合成後交由Skia引擎渲染處理為GPU資料,最後通過GL/Vulkan發給GPU。而我們要分析的地方就在Dart->
Layer Tree這裡。
- UI的基本流程
比如使用者一個輸入操作,可以理解發出為Vsunc訊號,這時,fliutter會先做Animation相關工作,然後Build當前UI,之後檢視開始佈局和繪製。生成檢視資料,但是隻會生成Layer Tree,並不能直接使用,還是需要Composite合成為一個Layer進行Rasterize光柵化處理。層級合併的原因是因為一般flutter的層級很多,直接把每一層傳給GPU傳遞,效率很低,所以會先做Composite,提高效率。光柵化之後才會給Flutter-Engine處理,這裡只是Framework層面的工作,所以看不到Engine,而我們分析的也只是Framework中的一小部分。
通過上面的講解,我們大概已經瞭解了flutter的繪圖的基本流程,但是我們並不清楚layout和paint做了什麼,而Widget是如何變成Layou Tree的。但是這裡內容太多,一句話說不清,所以我們還是先看下我們平時寫的大量Widget在flutter繪圖時的到底是啥用吧。
Widget在Flutter繪圖時的作用
在這之前,我們要先了解幾個概念
- Widget
- Element
- RenderObject
Widget
這裡的Widget就是我們平時寫的Widget,它是 Flutter中控制元件實現的基本單位。一個Widget裡面一般儲存了檢視的配置資訊,包括佈局、屬性等等。所以它只是一份直接使用的資料結構。在構建為結構樹,甚至重新建立和銷燬結構樹時都不存在明顯的效能問題。
Element
Element是Widget的抽象,它承載了檢視構建的上下文資料。flutter系統通過遍歷 Element樹來構建 RenderObject資料,所以Element是真正被使用的集合,Widget只是資料結構。比如檢視更新時,只會標記dirty Element,而不會標記dirty Widget。
RenderObject
我們要分析的Layout、Paint均發生在RenderObject中,並且LayerTree也是由RenderObject生成,可見其重要程度。所以 Flutter中大部分的繪圖效能優化發生在這裡。RenderObject樹構建的資料會被加入到 Engine所需的 LayerTree中。
而以上這三個概念也對應著三種樹結構:模型樹、呈現樹、渲染樹。在解釋他們的概念和關係以後,我們已經認識到RenderObject的重要性,因為以下Layout、Paint包括relayout boundary和repaint boundary都是在這裡發生的。一般一個Widget被更新,那麼持有該Widget的節點的Element會被標記為dirtyElement,在下一次更新介面時,Element樹的這一部分子樹便會被觸發performRebuild,在Element樹更新完成後,便能獲得RenderObject樹,接下來會進入Layout和Paint的流程。
Layout
- Layout的目的是要計算出每個節點所佔空間的真實大小。
在構建檢視樹的時候,節點的Constraints是自上而下的,但是計算layout是深度優先遍歷,這是因為節點通過Constraints並不一定能夠明確自己的size,有時它會依賴子節點的size,所以獲取size大小是自下而上。每個節點會接受到父物件的Constraints,子節點根據其來決定自己的大小,父物件會根據自己的邏輯決定子物件的位置來完成佈局。所以flutter的layout實際上就是這麼簡單的操作。那麼簡單肯定就有一些問題,比如某個節點的size變了,整個檢視樹就得重新計算?肯定不是這樣的,否則flutter就不存在圖形的高效能了。flutter是通過Relayout boundary來處理這樣的問題的。
- Relayout boundary
它的目的是提高flutter的繪圖效能,它的作用是設定測量邊界,邊界內的Widget做任何改變都不會導致邊界外重新計算並繪製。
當然它是有條件的,當滿足以下三個條件的任意一個就會觸發Relayout boundary
- constraints.isTight
- parentUsesSize == false
- sizedByParent == true
constraints.isTight
什麼是isTight呢?用BoxConstraints為例
它有四個屬性,分別是minWidth,maxWidth,minHeight,maxHeight
tight 如果最小約束(minWidth,minHeight)和最大約束(maxWidth,maxHeight)分別都是一樣的
loose 如果最小約束都是0.0(不管最大約束),如果最小約束和最大約束都是0.0,就同時是tightly和loose
bounded 如果最大約束都不是infinite
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’
所以為了達到有RelayoutBoundary的效果,可以將程式碼中的Container新增寬高以達到constraints.isTight條件,這個實驗就留給讀者自己測試吧。
Paint
paint的一個重要工作就是確定哪些Element放在同一Layer
佈局size計算是自下而上的,但是paint是自上而下的。在layout之後,所有的Widget的大小、位置都已經確定,這時不需要再做遍歷。
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
這樣強制使圖層分開,以達到毫不相關的控制元件的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)
參考
- Flutter’s Rendering Pipeline 推薦閱讀
- 深入瞭解Flutter介面開發
- Flutter 渲染流水線淺析
- Flutter原理與實踐
- Flutter中的佈局繪製流程簡析
- Flutter Dart Framework原理簡解
本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@Dpuntu
來源:https://juejin.im/post/5c0fc3cb5188251da07e09b3#heading-8