Flutter原理:三棵重要的樹(渲染過程、佈局約束、應用視圖的構建等)

轉發朋友的一篇文章~
java


點擊藍字、關注android

及時閱讀最新文章微信

這篇文章從 Flutter 框架層的三棵樹入手向你們層層剖析了 Flutter 中渲染組件的流程,從原理到實戰,但願對想要提高 Flutter 的讀者們有幫助。app

1

前言框架

瞭解 HTML 的讀者必定據說過 DOM 樹這個概念,它由頁面中每個控件組成,這些控件所造成的一種自然的嵌套關係使其能夠表示爲 「樹」 結構,咱們也能夠將這個概念應用在 Flutter 中,例如默認的計數器應用的結構以下圖:less



咱們也能夠看到上圖中每一個控件所造成的樹結構中隱含了一些關係,例如在上圖中,咱們能夠說 Text 組件是 Column 組件的子組件,Scaffold 是 AppBar 的父組件,這樣的層級關係使得每一個控件都清晰的鏈接到了一塊兒,樹結構由此而來。編輯器


在 flutter 中,Container、Text 等組件都屬於 Widget,因此咱們將這種樹稱爲 Widget 樹,也能夠叫作控件樹,它就表示了咱們在 dart 代碼中所寫的控件的結構。ide



然而,在 Flutter 體系結構中,真正作組件渲染在屏幕上這個任務的並不是在 控件層(Widget)層,而是在渲染(Rendering)層,那麼咱們在代碼中所寫組件又是怎麼經過渲染層顯示的呢?Flutter 中又引入了 Element 樹和 RenderingObject 樹兩棵樹。svg


Element 是什麼,咱們能夠把它稱之爲 Widget 另外一種抽象。讀者也能夠把它看做一個更爲實際控件,由於在咱們的手機屏幕上顯示的控件並不是咱們在代碼中所寫的 Widget,咱們在代碼中所使用的像 Container、Text 等這類組件和其屬性只不過是咱們想要構建的組件的配置信息,當咱們第一次調用 build() 方法想要在屏幕上顯示這些組件時,Flutter 會根據這些信息生成該 Widget 控件對應的 Element,一樣地,Element 也會被放到相應的 Element 樹當中。在 Flutter 中,一個 Widget 經過屢次複用能夠對應多個 Element 實例,Element 纔是咱們真正在屏幕上顯示的元素。函數

"

Element 與 Widget 另外一個區別在於,Widget 自然是不可變的(immutable),它如要更新便須要重建,若是想要把可變狀態與 Widget 關聯起來,可使用 StatefulWidget,StatefulWidget 經過使用StatefulWidget.createState 方法建立 State 對象,並將之擴充到 Element 以及合併到樹中。

"

爲了更爲深入的理解以上描述的含義,咱們能夠舉一個更爲形象的例子。Widget 做爲大 Boss,他把近期的戰略部署,即配置信息,寫在紙上下發給經理人 Element,Element 看到詳細的配置信息開始真正的開起活來了。咱們還須要注意一點,大 Boss 隨時會改變戰略部署,而後不會在原有的紙上修改而是從新寫下來,這時經理人爲了減小工做量須要將新的計劃與舊的計劃比較來做出相應的更新措施。這也是 Flutter 框架層作的一大優化。下面又來了,Element 做爲經理人也很體面,固然不會把活全乾完,因而又找了一個 RenderObject 的員工來幫它作粗重的累活。


RenderObject 在 Flutter 當中作組件佈局渲染的工做,其爲了組件間的渲染搭配及佈局約束也有對應的 RenderObject 樹,咱們也稱之爲渲染樹。


熟悉了 Flutter 中的上述三顆樹,相信讀者會對組件的渲染過程有了一個清晰的認識,這對咱們以後學習經常使用組件有很大的幫助,咱們須要用不一樣的眼光去看待咱們所創建的佈局和控件,以後咱們也會更加深刻的去理解其中更鮮爲人知的奧祕。

2

組件渲染過程簡述

從上文中,咱們知道控件樹中的每一個控件都會實現一個 RenderObject 對象作渲染任務,並將全部的 RenderObject 組成渲染樹。Flutter 渲染組件的過程以下:



Flutter 的渲染過程由用戶的輸入開始,當接受到用戶輸入的信號時,就會觸發動畫的進度更新,例如咱們第一次渲染時的啓動動畫,或者咱們在滾動手機屏幕時單個列表項複用時的移動動畫。以後便須要開始視圖數據的構建(build),這一步中 Flutter 建立了前文所描述的三棵視圖樹。


在這以後,視圖纔會進行佈局(layout),計算各個部分的大小,而後進行繪製(paint),生成每一個視圖的視覺數據,這部分的任務主要就是由 RenderObject 所作。這裏,Flutter 中的佈局過程可用下圖表示,在上述構建完成渲染樹後,父渲染對象會將佈局約束信息向下傳遞,子渲染對象根據本身的渲染狀況返回 Size,Size 數據會向上傳遞,最終父渲染對象完成佈局過程。



最後一步進行「光柵化」(Rasterize),前一步獲得合成的視圖數據其實仍是一份矢量描述數據,光柵化幫助把這份數據真正地生成一個一個的像素填充數據。在 Flutter 中,光柵化這個步驟被放在了 Engine 層中。


在平常開發學習中,咱們只須要在代碼層配置好咱們的 Widget 樹,瞭解各類 Widget 特性及使用方法,其他的工做均可以交給咱們的框架層去實現。

3

元素樹詳解

咱們已經知道了各種控件的做用及其使用方法,這些 Widget 被咱們開發人員配置了多個屬性來定義它的展示形式,例如配置 Text 組件須要顯示的字符串,配置輸入框組件須要顯示的內容。咱們 Element 樹會記錄這些配置信息。熟悉 React 的讀者可能瞭解過其中的 「虛擬 DOM」 這個概念,上述 Flutter 這種操做也正體現了這一律念。Widget 是不可變,它的改變就意味着要重建,而其重建也很是頻繁,若是咱們將更多的任務都交給它將會對性能形成很大的損傷,所以咱們把 Widget 組件看成一個虛擬的組件樹,而真正被渲染在屏幕上的實際上是 Elememt 這棵樹,它持有其對應 Widget 的引用,若是他對應的 Widget 發生改變,它就會被標記爲 dirty Element,因而下一次更新視圖時根據這個狀態只更新被修改的內容,從而達到提高性能的效果。


每次,當控件掛載到控件樹上時,Flutter 調用其 createElement() 方法,建立其對應的 Element。Flutter 再將這個 Element 放到元素樹上,並持有建立它控件的引用,以下圖:



控件會有它的子樹:



子控件也會建立相應 Element 被放在元素樹上:


4

Element 中的狀態

咱們上文提到了 Widget 的不可變性,相應的 Element 就有其可變性,正如咱們前文所說的它被標記爲 dirty Element 即是做爲須要更新的狀態,另一個咱們須要格外注意的是,有狀態組件(StatefulWidget)對應的 State 對象其實也被 Element 所管理,以下圖所示。



Flutter 中的 Widget 一直在重建,每次重建以後,Element 都會採用相應的措施來肯定是否我對應的新控件跟以前引用舊控件是否有所改變,若是沒改變則只須要作更新操做,若是先後不一樣則會重建立。那麼,Element 根據什麼來肯定控件是否改變呢?它會比較 Widget 如下兩個屬性:


- 組件類型

- Widget 的 Key (若是有)


組件類型即先後控件的是不是同一個類所建立的,Key 即爲每一個控件的惟一標識。

5

渲染樹詳解

咱們已經大體知道 Flutter 中的三棵重要的樹及 Element 樹的工做原理,其中第三棵渲染樹的任務就是作組件的具體的佈局渲染工做。

渲染樹上每一個節點都是一個繼承自 RenderObject 類的對象,其由 Element 中的 renderObject 或  RenderObjectWidget 中的 createRenderObject 方法生成,該對象內部提供多個屬性及方法來幫助框架層中的組件如何佈局渲染。

"

咱們知道 StatelessWidget 和 StatefulWidget 兩種直接繼承自 Widget 的類,在 Flutter 中,還有另外一個類 RenderObjectWidget 也一樣直接繼承自 Widget,它沒有 build 方法,可經過 createRenderObject 直接建立 RenderObject 對象放入渲染樹中。Column 和 Row 等控件都間接繼承自 RenderObjectWidget

"

主要屬性和方法以下:


- constraints 對象,從其父級傳遞給它的約束

- parentData 對象,其父對象附加有用的信息。

- performLayout 方法,計算此渲染對象的佈局。

- paint 方法,繪製該組件及其子組件。


RenderObject 做爲一個抽象類。每一個節點須要實現它才能進行實際渲染。擴展 RenderOject 的兩個最重要的類是RenderBox 和 RenderSliver。這兩個類分別是應用了 Box 協議和 Sliver 協議這兩種佈局協議的全部渲染對象的父類,其還擴展了數十個和其餘幾個處理特定場景的類,並實現了渲染過程的細節,如 RenderShiftedBox 和 RenderStack 等等。

佈局約束

在上面,咱們介紹組件渲染流程時,咱們瞭解到了 Flutter 中的控件在屏幕上繪製渲染以前須要先進行佈局(Layout)操做。其具體可分爲兩個線性過程:從頂部向下傳遞約束,從底部向上傳遞佈局信息,其過程可用下圖表示。



第一個線性過程用於傳遞佈局約束。父節點給每一個子節點傳遞約束,這些約束是每一個子節點在佈局階段必需要遵照的規則。就好像父母告訴本身的孩子 :「你必須遵照學校的規定,才能夠作其餘的事」。常見的約束包括規定子節點最大最小寬度或者子節點最大最小的高度。這種約束會向下延伸,子組件也會產生約束傳遞給本身的孩子,一直到葉子結點。


第二的線性過程用來傳遞具體的佈局信息。子節點接受到來自父節點的約束後,會依據它產生本身具體的佈局信息,如父節點規定個人最小寬度是 500 的單位像素,子節點按照這個規則可能定義本身的寬度爲 500 個像素,或者大於 500 像素的任何一個值。這樣,肯定好本身的佈局信息以後,將這些信息告訴父節點。父節點也會繼續此操做向上傳遞一直到最頂部。


下面咱們具體介紹有哪些具體的佈局約束可在樹中傳遞。Flutter 中有兩種主要的佈局協議:Box 盒子協議和 Sliver 滑動協議。這裏咱們以盒子協議爲例展開具體的介紹。


在盒子協議中,父節點傳遞給其子節點的約束爲 BoxConstraints。該約束規定了容許每一個子節點的最大和最小寬度和高度。以下圖,父節點傳入 Min Width 爲 150,Max Width 爲 300 的 BoxConstraints:



當子節點接受到該約束,即可以取得上圖中綠色範圍內的值,即寬度在 150 到 300 之間,高度大於 100,當取得具體的值以後再將取得具體的大小的值上傳給父節點,從而達到父子的佈局通訊。

6

自定義一個 Center 控件

如今,咱們能夠應用前文中提到的佈局約束與渲染樹相關的概念本身定義一個相似居中佈局的組件 RenderObject 對象渲染在屏幕上。

因此咱們稱本身自定義組件爲 CustomCenter

void main() { runApp(MaterialApp( home: Scaffold( body: Container( color: Colors.blue, constraints: BoxConstraints( maxWidth: double.infinity, minWidth: 100.0, maxHeight: double.infinity, minHeight: 100.0), child: CustomCenter( child: Container( color: Colors.red, ), ), ), ), ));}

如今咱們來實現咱們的 CustomCenter:

class CustomCenter extends SingleChildRenderObjectWidget { Stingy({Widget child}) : super(child: child);
@override RenderObject createRenderObject(BuildContext context) { // TODO: implement createRenderObject return RenderCustomCenter(); }}

CustomCenter 繼承了

SingleChildRenderObjectWidget,代表這個 Widget 只能有一個子控件,其中,createRenderObject(...) 方法用於真正建立並返回咱們的 RenderObject 對象實例, 咱們的 RenderObject 爲 RenderCustomCenter,代碼以下:

class RenderCustomCenter extends RenderShiftedBox { RenderStingy() : super(null);
// 重寫繪製方法 @override void paint(PaintingContext context, Offset offset) { // TODO: implement paint super.paint(context, offset); }
// 重寫佈局方法 @override void performLayout() { // 佈局子元素並向下傳遞佈局約束 child.layout( BoxConstraints( minHeight: 0.0, maxHeight: constraints.minHeight, minWidth: 0.0, maxWidth: constraints.minWidth), parentUsesSize: true);
print('constraints: $constraints');
// 指定子元素的偏移位置 final BoxParentData childParentData = child.parentData; childParentData.offset = Offset((constraints.maxWidth - child.size.width)/2, (constraints.maxHeight - child.size.height)/2); print('childParentData: $childParentData');
// 定義本身(CustomCenter)的大小,這裏選擇約束對象的最大值 size = Size(constraints.maxWidth, constraints.maxHeight); print('size: $size'); }}

RenderCustomCenter 繼承自 

RenderShiftedBox,該類是繼承自 RenderBox。RenderShiftedBox 知足盒子協議,而且提供了 performLayout() 方法的實現。咱們須要在 performLayout() 方法中佈局咱們的子元素。


咱們在使用 child.layout(...) 方法佈局 child 的時候傳遞了兩個參數,第一個爲 child 的佈局約束,而另一個參數是 parentUserSize, 該參數若是設置爲 false,則意味着 parent 不關心 child 選擇的大小,這對佈局優化比較有用;由於若是 child 改變了本身的大小,parent 就沒必要從新 layout 了。可是在咱們的例子中,咱們的須要把 child 放置在 parent 的中心,就是 child 的大小(Size)一旦改變,則其對應的偏移量(Offset) 也會改變,因而 parent 須要從新佈局,因此咱們這裏傳遞了一個 true。


當 child.layout(...) 完成了之後,child 就肯定了本身的 Layout Details。而後咱們就還能夠爲其設置偏移量來將它放置到咱們想放的位置。在咱們的例子中爲 居中。


最後,和 child 根據 parent 傳遞過來的約束選擇了一個尺寸同樣,咱們也須要爲 CustomCenter 選擇一個尺寸。


運行效果以下:


7

應用視圖的構建

Flutter App 入口的部分發生於以下代碼:

import 'package:flutter/material.dart';
// 這裏的 MyApp是一個 Widgetvoid main() => runApp(new MyApp());

runApp函數接受一個 Widget類型的對象做爲參數,也就是說在 Flutter的概念中,只存在 View,而其餘的任何邏輯都只爲 View的數據、狀態改變服務,不存在 ViewController(或者叫 Activity)。接下來看 runApp作了什麼:

void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() ..attachRootWidget(app) ..scheduleWarmUpFrame();}class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding { static WidgetsBinding ensureInitialized() { if (WidgetsBinding.instance == null) new WidgetsFlutterBinding(); return WidgetsBinding.instance; }}

在runApp中,傳入的 widget 被掛載到根 widget 上。這個 WidgetsFlutterBinding 實際上是一個單例,經過 mixin 來使用框架中實現的其餘 binding 的 Service,好比手勢、基礎服務、隊列、繪圖等等。而後會調用 scheduleWarmUpFrame 這個方法,從這個方法註釋可知,調用這個方法會主動構建視圖數據。這樣作的好處是由於 Flutter 依賴 Dart 的 MicroTask 來進行幀數據構建任務的 schedule,這裏經過主動調用進行整個週期的 「熱身」,這樣最近的下次 VSync 信號同步時就有視圖數據可提供,而不用等到 MicroTask 的 next Tick。


而後咱們再來看 attachRootWidget 這個函數幹了什麼:

void attachRootWidget(Widget rootWidget) { _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement);}

attachRootWidget 把 widget交給了 RenderObjectToWidgetAdapter這座橋樑,經過這座橋樑,Element 被建立,而且同時能持有 Widget 和 RenderObject的引用。而後咱們從上文就知道後面發生的就是第一次的視圖數據構建了。


從這一部分能印證了:Flutter應用經過 Widget、Element、RenderObject 三種樹結構來維護整個應用的視圖數據

END

技術交流,歡迎添加個人微信



「在看」我嗎?


本文分享自微信公衆號 - Android羣英傳(android_heroes)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索