Widget Tree,Element Tree,RenderObject Tree,稍微接觸過Flutter一段時間的朋友應該或多或少都聽過這三顆鼎鼎大名的渲染樹。如果也你和我一樣,曾經滿懷好奇的查了一下文件,然後立刻就被它們之間複雜的依賴關係和呼叫流程勸退,心想「好喔其實我也不是真的那麼想知道」,或是你很認真的看完許多文件或影片,感覺自己還是似懂非懂,今天你可以和我一起試著再給它一次機會,搞不好換個人換個說法就懂了。雖然在絕大多數的情況下,這些底層實作對你的開發工作幾乎不會有什麼影響,瞭解這些也不會讓你的UI效能突飛猛進,但瞭解它還是可以1.滿足你的好奇心,2.讓你在聚會/面試被問到的時候可以侃侃而談,畢竟面試不就是喜歡考些奇怪的Tree資料結構和演算法,Flutter工作問個Flutter Tree也是很合情合理。
總之,讓我們先從一個最簡單的APP開始:
void main() {
runApp(RichText(
text: TextSpan(text: "Hello, World!"),
textDirection: TextDirection.ltr,
));
}
這可以說是Flutter真正的Hello World,由單一一個RichText
組成我們的Widget Tree。那麼這顆樹是如何被渲染出來的呢?我們可以來看看RichText
裡有什麼有趣的東西:
這兩個RichText
的函數告訴我們,Widget會在某個時間點被某個類別呼叫,建立或更新某個叫做RenderObject
的物件,而這個物件接收了來自Widget的所有UI設定。另外從RenderObject
這個名稱,以及updateRenderObject
中的命令式更新法,我們可以猜測它可能是個類似於Android View或iOS UIView,背負計算排版尺寸和實際渲染等重責大任的類別。為了確認這件事,讓我們繼續挖下去:
class RenderParagraph extends RenderBox
abstract class RenderBox extends RenderObject
進入RichText
建立的RenderParagraph
,我們除了找到最重要的performLayout
和paint
,也看到它還有包括各種compute在內,極多的渲染相關成員變數和函數,再次確認了RenderObject
的確是扮演著類似原生View/UIView,包山包海責任重大的角色。也就是說,建立和更新這個物件很貴,而我們絕對會希望盡量避免去重複建立它,或進行不必要的更新。如何做到這一點?
在此之前我們先回想一下,其實原生的View/UIView本來就沒有整天在重建的,為什麼?因為那時候UI更新是命令式的,例如想改變文字就直接TextView.setText
就好了。然而Flutter擁抱了陳述式開發,根本禁止我們直接修改RenderObject
的參數,那該怎麼辦?我們只好遵循陳述式的UI = f(state),在每次更新的時候,重新建立一份完整包含所有參數、不會被改變的UI設定檔,並透過它來更新RenderObject
。而這個所謂的UI設定檔,就是我們最熟悉的Widget
。它的結構基本上跟一個JSON差不了多少,只是多了靜態型別的輔助。即使偶爾有些簡單邏輯,也大多是在幫助我們用簡單的設定做出更完整的設定(ex.Container
)。這也就是為什麼Widget的建立和回收極為便宜,讓我們可以在build函數中隨意的建立一大串Widget Tree,也不須要去cache建好的Widget。
話說從頭,Widget到底是在什麼時候,被什麼類別呼叫來建立RenderObject
的?讓我們繼續看下去。
abstract class RenderObjectElement extends Element {
@override
void mount(Element parent, dynamic newSlot) {
...
_renderObject = widget.createRenderObject(this);
...
_dirty = false;
}
}
追蹤之後我們在RenderObjectElement
的mount
函數裡面找到了widget.createRenderObject(this)
。這個Element
是什麼?來看看官方文件怎麼說:
An instantiation of a [Widget] at a particular location in the tree.
Widgets describe how to configure a subtree but the same widget can be used
to configure multiple subtrees simultaneously because widgets are immutable.
An [Element] represents the use of a widget to configure a specific location
in the tree. Over time, the widget associated with a given element can
change, for example, if the parent widget rebuilds and creates a new widget
for this location.
簡單來說Element
是Widget
這個UI設定檔實例化成的UI元素,既然Widget
只是個設定檔,自然可以用同一份設定檔在UI樹上的不同位置產生個別的Element
。講到實例化我們可能又會想起Android XML->View或iOS xib->UIView,難道Element
又是跟View/UIView類似的東西嗎?如果說RenderObject
已經負擔了以往View/UIView中渲染的工作,那麼Element
負擔的又是什麼?其實在常見的UI系統中,還有一塊非常重要的領域我們一直沒有提到,那就是UI元件生命週期的管理,而這正是Element
所負責的工作。
我們可以看到,Element
的確包含了許多生命週期相關的函數。有趣的是,Element
同時也保有Widget
和RenderObject
的參照,代表它同時肩負了由這三者組成的,一個完整UI元件的生命週期管理。那麼,這個Element
又是由誰在什麼時候建立的?
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
abstract class Widget extends DiagnosticableTree {
@protected
@factory
Element createElement();
}
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
if (element == null) {
...
element = createElement(); // HERE
...
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
abstract class Element extends DiagnosticableTree implements BuildContext {
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
....
final Element newChild = newWidget.createElement(); // AND HERE
....
}
}
我們最後在Widget
的定義中找到createElement
這個函數,並發現它在RenderObjectToWidgetAdapter.attachToRenderTree
和Element.inflateWidget
中被呼叫。這裡的RenderObjectToWidgetAdapter
是整個Flutter中的第一個Widget
,也是由它來建立Element Tree中的第一個Element
。之後的Child Element都是由Parent Element來建立的。
同時我們也發現不同的Widget
會建立其對應的Element
,並將自身傳入作為參數。這裡的MultiChildRenderObjectElement
自然代表它可以有多個child Element,而每一個還沒被建立Child Element的空位就是inflateWidget
的newSlot
,這個空位在Widget Tree上對應位置的Widget
會被傳入用來建立Element
。
到這裡我們應該可以整理出一個很簡略的流程了。首先APP啟動時由RenderObjectToWidgetAdapter
,根據我們傳入runApp
的Widget
建立第一個Element
。此Element
接收Widget
的參照,呼叫widget.createRenderObject
來建立RenderObject
。接著此Element
在inflateWidget
時,利用自己child空位對應的Widget
建立出Child Element。而Child Element又繼續呼叫widget.createRenderObject
來建立RenderObject
。此流程不斷重複直到遍歷整顆Widget Tree。
這次我們試著不直接丟出三顆樹的流程架構,而是從最簡單的Hello World開始,透過追查原始碼的方式,一步步摸索出三顆樹的對應關係。因為希望能把各種前因後果解釋清楚,花了比預想還多得多的篇幅在基礎的部份,希望會對一些人有幫助。當然其實關於這三顆樹還有很多細節和衍生的部份沒有提到,下次有空再繼續吧。