一、引子
Flutter 中有三棵樹:Widget 樹,Element 樹和 RenderObject 樹。當應用啟動時 Flutter 會遍歷并創建所有的 Widget 形成 Widget Tree,同時與 Widget Tree 相對應,通過調用 Widget 上的 createElement() 方法創建每個 Element 對象,形成 Element Tree。最后調用 Element 的 createRenderObject() 方法創建每個渲染對象,形成一個 Render Tree。 Element就是Widget在UI樹具體位置的一個實例化對象,大多數Element只有唯一的renderObject,但還有一些Element會有多個子節點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有Element的RenderObject構成一棵樹,我們稱之為”Render Tree“即”渲染樹“。總結一下,我們可以認為Flutter的UI系統包含三棵樹:Widget樹、Element樹、渲染樹。他們的依賴關系是:根據Widget樹生成Element樹,再依賴于Element樹生成RenderObject 樹,如下圖:
這種樹形結構類似于HTML中的DOM樹,如默認的計數器應用的結構如下圖:
在 flutter 中,Container、Text 等組件都屬于 Widget,所以這課樹就是 Widget 樹,也可以叫做控件樹,它就表示了我們在 dart 代碼中所寫的控件的結構。Element 就是 Widget 的另一種抽象。我們在代碼中使用的像 Container、Text 等這類組件和其屬性只不過是我們想要構建的組件的配置信息,當我們第一次調用 build()`方法想要在屏幕上顯示這些組件時,Flutter 會根據這些信息生成該 Widget 控件對應的 Element,同樣地,Element 也會被放到相應的 Element 樹當中。RenderObject 在 Flutter 當中做組件布局渲染的工作,其為了組件間的渲染搭配及布局約束也有對應的 RenderObject 樹,我們也稱之為渲染樹。
二、Widget 樹
Widget 是 Flutter 的核心部分,是用戶界面的不可變描述信息。Widget的功能是“描述一個UI元素的配置數據”,它就是說,Widget其實并不是表示最終繪制在設備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數據。正如 Flutter 的口號 Everything’s a widget, 用 Flutter 開發應用就是在寫 Widget 。Widget 的 canUpdate 方法通過比較新部件和舊部件的 runtimeType 和 key 屬性是否相同來決定更新部件對應的 Element。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
@protected
Element createElement();
三、Element 樹
實際上,Flutter中真正代表屏幕上顯示元素的類是Element,Widget只是UI元素的一個配置數據,并且一個Widget可以對應多個Element。Element就是Widget在UI樹具體位置的一個實例化對象,大多數Element只有唯一的renderObject,但還有一些Element會有多個子節點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。Widget 是不可變,它的改變就意味著要重建,而其重建也非常頻繁,如果我們將更多的任務都交給它將會對性能造成很大的損傷,因此我們把 Widget 組件當作一個虛擬的組件樹,而真正被渲染在屏幕上的其實是 Elememt 這棵樹,它持有其對應 Widget 的引用,如果他對應的 Widget 發生改變,它就會被標記為 dirty Element,于是下一次更新視圖時根據這個狀態只更新被修改的內容,從而達到提升性能的效果。
Element的生命周期如下:
Framework 調用Widget.createElement 創建一個Element實例,記為element
Framework 調用 element.mount(parentElement,newSlot) ,mount方法中首先調用element所對應Widget的createRenderObject方法創建與element相關聯的RenderObject對象,然后調用element.attachRenderObject方法將element.renderObject添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發生在Element樹結構發生變化時才需要重新attach)。插入到渲染樹后的element就處于“active”狀態,處于“active”狀態后就可以顯示在屏幕上了(可以隱藏)。
當有父Widget的配置數據改變時,同時其State.build返回的Widget結構與之前不同,此時就需要重新構建對應的Element樹。為了進行Element復用,在Element重新構建前會先嘗試是否可以復用舊樹上相同位置的element,element節點在更新前都會調用其對應Widget的canUpdate方法,如果返回true,則復用舊Element,舊的Element會使用新Widget配置數據更新,反之則會創建一個新的Element。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時相等,如果同時相等就返回true,否則就會返回false。根據這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。
當有祖先Element決定要移除element 時(如Widget樹結構發生了變化,導致element對應的Widget被移除),這時該祖先Element就會調用deactivateChild 方法來移除它,移除后element.renderObject也會被從渲染樹中移除,然后Framework會調用element.deactivate 方法,這時element狀態變為“inactive”狀態。
“inactive”態的element將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定element,“inactive”態的element在當前動畫最后一幀結束前都會保留,如果在動畫執行結束后它還未能重新變成“active”狀態,Framework就會調用其unmount方法將其徹底移除,這時element的狀態為defunct,它將永遠不會再被插入到樹中。
如果element要重新插入到Element樹的其它位置,如element或element的祖先擁有一個GlobalKey(用于全局復用元素),那么Framework會先將element從現有位置移除,然后再調用其activate方法,并將其renderObject重新attach到渲染樹。
Element樹的生命周期如圖:
四、RenderObject 樹(渲染樹)
4.1 介紹
渲染樹的任務就是做組件的具體的布局渲染工作,渲染樹上每個節點都是一個繼承自 RenderObject 類的對象,其由 Element 中的 renderObject 或 RenderObjectWidget 中的 createRenderObject 方法生成,該對象內部提供多個屬性及方法來幫助框架層中的組件如何布局渲染。RenderObject 用于應用界面的布局和繪制,保存了元素的大小,布局等信息,實例化一個 RenderObject 是非常耗能的。 RenderObject 主要屬性和方法如下:
constraints 對象,從其父級傳遞給它的約束。
parentData 對象,其父對象附加有用的信息。
performLayout 方法,計算此渲染對象的布局。
paint 方法,繪制該組件及其子組件。
4.2 布局過程
Flutter 中的控件在屏幕上繪制渲染之前需要先進行布局(Layout)操作。其具體可分為兩個線性過程:
- 從頂部向下傳遞約束。
這一過程用于傳遞布局約束。父節點給每個子節點傳遞約束,這些約束是每個子節點在布局階段必須要遵守的規則。常見的約束包括規定子節點最大最小寬度或者子節點最大最小的高度。這種約束會向下延伸,子組件也會產生約束傳遞給自己的孩子,一直到葉子結點。
- 從底部向上傳遞布局信息。
這一過程用來傳遞具體的布局信息。子節點接受到來自父節點的約束后,會依據它產生自己具體的布局信息,如父節點規定我的最小寬度是 500 的單位像素,子節點按照這個規則可能定義自己的寬度為 500 個像素,或者大于 500 像素的任何一個值。這樣,確定好自己的布局信息之后,將這些信息告訴父節點。父節點也會繼續此操作向上傳遞一直到最頂部。 其過程可用下圖表示:
Flutter 中有兩種主要的布局協議:Box 盒子協議和 Sliver 滑動協議。 在RenderBox 中,有個size屬性用來保存控件的寬和高。RenderBox的layout是通過在組件樹中從上往下傳遞BoxConstraints對象的實現的。BoxConstraints對象可以限制子節點的最大和最小寬高,子節點必須遵守父節點給定的限制條件。在布局階段,父節點會調用子節點的layout()方法,layout方法需要傳入兩個參數,第一個為constraints,即 父節點對子節點大小的限制,該值根據父節點的布局邏輯確定。另外一個參數是 parentUsesSize,該值用于確定 relayoutBoundary,該參數表示子節點布局變化是否影響父節點,如果為true,當子節點布局發生變化時父節點都會標記為需要重新布局,如果為false,則子節點布局發生變化后不會影響父節點。
4.3 繪制過程
RenderObject可以通過paint()方法來完成具體繪制邏輯,流程和布局流程相似,子類可以實現paint()方法來完成自身的繪制邏輯,paint()簽名如下:
void paint(PaintingContext context, Offset offset) { }
通過context.canvas可以取到Canvas對象,接下來就可以調用Canvas API來實現具體的繪制邏輯。如果節點有子節點,它除了完成自身繪制邏輯之外,還要通過paintChild()方法來調用子節點的繪制方法。如此遞歸完成整個節點樹的繪制,最終調用棧為: paint() > paintChild() > paint() ... 。
五、為什么需要三棵樹?
先說答案:使用三棵樹的目的是盡可能復用 Element。
復用 Element 對性能非常重要,因為 Element 擁有兩份關鍵數據:Stateful widget 的狀態對象及底層的 RenderObject。當應用的結構很簡單時,或許體現不出這種優勢,一旦應用復雜起來,構成頁面的元素越來越多,重新創建 3 棵樹的代價是很高的,所以需要最小化更新操作。當 Flutter 能夠復用 Element 時,用戶界面的邏輯狀態信息是不變的,并且可以重用之前計算的布局信息,避免遍歷整棵樹。
參考:
https://book.flutterchina.club/
https://juejin.cn/post/6844903837858283528