iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 5

days[4] = "三顆渲染樹是如何運作的?"

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

https://ithelp.ithome.com.tw/upload/images/20200905/20129053d1pwUEqQJV.png
進入RichText建立的RenderParagraph,我們除了找到最重要的performLayoutpaint,也看到它還有包括各種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;
  }
}

追蹤之後我們在RenderObjectElementmount函數裡面找到了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.

簡單來說ElementWidget這個UI設定檔實例化成的UI元素,既然Widget只是個設定檔,自然可以用同一份設定檔在UI樹上的不同位置產生個別的Element。講到實例化我們可能又會想起Android XML->View或iOS xib->UIView,難道Element又是跟View/UIView類似的東西嗎?如果說RenderObject已經負擔了以往View/UIView中渲染的工作,那麼Element負擔的又是什麼?其實在常見的UI系統中,還有一塊非常重要的領域我們一直沒有提到,那就是UI元件生命週期的管理,而這正是Element所負責的工作。
https://ithelp.ithome.com.tw/upload/images/20200905/20129053MRhfoJHzfH.png
我們可以看到,Element的確包含了許多生命週期相關的函數。有趣的是,Element同時也保有WidgetRenderObject的參照,代表它同時肩負了由這三者組成的,一個完整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.attachToRenderTreeElement.inflateWidget中被呼叫。這裡的RenderObjectToWidgetAdapter是整個Flutter中的第一個Widget,也是由它來建立Element Tree中的第一個Element。之後的Child Element都是由Parent Element來建立的。

同時我們也發現不同的Widget會建立其對應的Element,並將自身傳入作為參數。這裡的MultiChildRenderObjectElement自然代表它可以有多個child Element,而每一個還沒被建立Child Element的空位就是inflateWidgetnewSlot,這個空位在Widget Tree上對應位置的Widget會被傳入用來建立Element

到這裡我們應該可以整理出一個很簡略的流程了。首先APP啟動時由RenderObjectToWidgetAdapter,根據我們傳入runAppWidget建立第一個Element。此Element接收Widget的參照,呼叫widget.createRenderObject來建立RenderObject。接著此ElementinflateWidget時,利用自己child空位對應的Widget建立出Child Element。而Child Element又繼續呼叫widget.createRenderObject來建立RenderObject。此流程不斷重複直到遍歷整顆Widget Tree。


這次我們試著不直接丟出三顆樹的流程架構,而是從最簡單的Hello World開始,透過追查原始碼的方式,一步步摸索出三顆樹的對應關係。因為希望能把各種前因後果解釋清楚,花了比預想還多得多的篇幅在基礎的部份,希望會對一些人有幫助。當然其實關於這三顆樹還有很多細節和衍生的部份沒有提到,下次有空再繼續吧。


上一篇
days[3] = "為什麼需要狀態管理?"
下一篇
days[5] = "三顆渲染樹是如何運作的?(二)"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言