Flutter render 的那一兩件事情

Jast Lai
Jastzeonic
Published in
16 min readOct 22, 2021

前言

上回寫了 Jetpack compose 後,我很好奇,為什麼 compose 的 composable 可以像不用錢似的一直 instance。

我想起了之前稍微碰了一下的 Flutter ,原則上 Jetpack compose 和 Flutter 都是 Google 的攣生兄弟,他們兩個都是都是宣告式的語法,然後因為 Widget 和 composable 都被設定為不可異動,所以更新它們 UI 的方式好像不用錢似的每次刷新每次都是一個新的 instance 。

這令我好奇心蠢蠢欲動,於是我開始追 Code ,然後看文件,追一些文章,後來理出這篇文章,應該可以稍微解釋一下,究竟是甚麼原因,可以讓 Widget 這些東西可以不斷被重新 instance。

先來看看怎麼建立一個 App

Dart 是從 main 開始的,透過 runApp 這個 method 去把做為 app 的Widget 給建構出來 -

寫 Android 寫久了,看到 main 會莫名的覺得有點新鮮。不過 Android 沒 main 是 Android 那很不一樣的架構所致的,否則 main 應該都會作為大多數應用程式起始點,至於 Android App 那很不一樣的起始那又是另一個故事了。

可以注意到, runApp 本身是一個 top-level-functions,那他會做的事情是去初始化建構一個 WidgetsFlutterBinding ,那麼之後 Widget 的更新等等操作都會在這邊去做處理。

先不要管排程這件事情,總之這裡 runApp 要做的事情呢,就是要把傳進來的 RichText 當作是 rootWidget

要怎麼做才能成為 root widget 呢?追下去,可以注意到,這裡要利用這個 widget 去建立 element,在 attachRootWidget 的時候,這裡實作了一個叫做 RenderObjectToWidgetAdapter 的 widget,並把 RichText 作為 child 傳入,接著呼叫了一個叫做 attachToRenderTree 的地方。

只是這時的 renderViewElement 是伴隨著 WidgetsFlutterBinding 被宣告的,但還沒有被實作,所以會是 null,那會發生甚麼事情呢?

他會去呼叫 createElement()

createElement() 會實作並回傳一個叫做 RenderObjectToWidgetElement 的 Element,並把 RenderObjectToWidgetAdapter 作為 Property 傳入。

接著看下去,會發現被建立出來的 Element 呼叫了 mount ,這裡應該是安裝的意思,那來看一下 mount 具體做了甚麼事情:

可以看到這邊利用剛才作為 property 傳入的 RenderObjectToWidgetAdapter 呼叫 createRenderObject,並且把 Element 當成 Property 傳入他的 constructor 之中。

看到這邊會發現,三個在 Flutter render 系統的大名鼎鼎的 Widget 、Element、RenderObject 都出現了,他們之間的關係真的不是普通的亂,但還沒完呢。

呼叫了 RenderObjectToWidgetAdapter 的 createRenderObject 拿到的會是甚麼?是隨著 WidgetsFlutterBinding 一同產生一個叫做 renderView 的 RenderObject。

這個 container 正是 RenderObjectWidgetAdapter 在實作產生 RenderObjectToWidgetElement 傳入的 renderView,這個怎麼來的待會解釋。

做完上面這些行為後,RenderObjectToWidgetElement 會在尾巴呼叫一個叫做 _rebuild 的 private method。

可以看到這時的會做一個 update child 的動作。

這時只要知道 widget.child 是誰呢?先來看看 widget 是誰?沒錯,就是 RenderObjectToWidgetAdapter ,那 RenderObjectToWidgetAdapter 的 child 是誰?一卡檸檬,正是 RichText。

可以發現會進到 inflateWidget

裏頭會用 RichText 這個 Widget 呼叫 createElement ,建立一個 MultiChildRenderObjectElement ,同時會把自己當成 property 傳進去 MultiChildRenderObjectElement 裏頭。

接著會用 MultiChildRenderObjectElement 做 mount ,這裡只要知道 this 就是 RenderView,作為 parent 傳入這個 method。

很有趣,似乎又繞回了 RenderObjectElement (RenderObjectElement 是 RenderObjectToWidgetElement 的繼承類別,但這邊先別管這個),那可以注意到這裡利用 widget 。也就是 RichText 建立了一個 RenderObject 叫做 RenderParagraph ,記得是由 MultiChildRenderObjectElement 拿著 RenderObject,而非 widget。

接著來到 attachRenderObject

這邊會找一個 Ancestor (祖先) 的 RenderObjectEelement ,還記得傳進來的 parent 是誰嗎?沒錯,正是 RenderObjectToWidgetElement。

RenderObjectToWidgetElement 的 RenderObject 是誰呢?正是 WidgetsFlutterBinding 的 renderView 。 此時終於把 RichText 的 RenderObject 和 WidgetsFlutterBinding 的 RenderObject renderView 綁在一起了。

簡單的整理一下我們上面得到的資訊,

  • 塞了一個 Widget 給 runApp
  • 產生一個 WidgetsFlutterBinding (如果現存為 null 的話) 附帶一個 renderView ,他正是 WidgetsFlutterBinding 的 RenderObject
  • 產生 RenderObjectToWidgetAdapter 。
  • 利用 RenderObjectToWidgetAdapter 產生 RenderObjectToWidgetElement 並將 RenderObjectToWidgetAdapter 作為 RenderObjectToWidgetElement 的 property。
  • RenderObjectToWidgetElement 會產生當初 runApp 傳入的 Widget 的 Element 。
  • 利用 Widget 產生出來的 Element 透過 Widget 產生 RenderObject。
  • 利用 RenderObjectToWidgetElement 將 Widget 的 RenderObject 變成 WidgetsFlutterBinding 的 renderView 的 child ,並將其作為 root widget。

好,知道這些,我們可以了解到 RenderObject 是透過 Element 利用 Widget 產生出來的,而 Element 則是由 Widget 產生出來的。

在做完綁定後,接著的 scheduleWarmUpFrame 就會開始把 widget 提供的資訊 render 畫面了。

那 RenderObject 是啥玩意阿?

說到 RenderObject ,那可以來看看 RenderView 是幹嘛的。

在 runApp 時,建立了 WidgetsFlutterBinding ,那這個 WidgetsFlutterBinding 同時含有一個叫做 RenderBinding 的 mixin

那麼 RenderBinding 就在生成的過程中同時產生了 RenderView。

那顯然 RenderView 會負責把他的 Child 給畫到畫面上。

那會怎麼畫取決於 Child 是甚麼樣的 RenderObject:

RenderObject 在呼叫 PainWithContext 後會呼叫到 paint ,以 RichText 來說,他的 RenderObject 是 RenderParagraph ,那他的 paint 大概是這樣:

有玩過 Android View 人對上面的 canvas 阿,drawRect 等等應該都不陌生。

可見 RenderView 就是 runApp 時所產生,算是一個 App 裡原初的 RenderObject。

像是 Column ,他的 RenderObject 叫做 RenderFlex ,RenderFlex 的 paint 會呼叫 defaultPaint ,那 defaultPaint 就會開始去呼叫他的 child 並繪製他。

講到這裡,如果對 View Tree 有概念的話,腦海應該浮現了一個樹狀圖。

我以這個簡單的 MaterialApp 來舉例:

MaterialApp 是 StatefulWidget ,StatefulWidget 和 StatelessWidget 跟 RichText 比較不一樣,它們並不需要建立 RenderObject ,主要是因為他們要 Render 的對象是它們提供出來的 Widget ,這邊不深究(雖然待會會提及一點 State 的部分),所以這個簡單的 App 的 RenderObject tree 只要看 _MyHomePageState build 的部分。

假如 RichText 是一個 Android 的 View ,那麼在 Android 裏頭,會用 findViewById 去把它弄成一個 field 然後放到 control flow (Activity, fragment) 去操作。

又回到了最初的問題了,這樣我建立一個 Widget 豈不是每次都要重新建一個 RenderObject 然後去 layout 接著 draw 。

StatefullWidget 呼叫一次 setState 成本不是嚇死人了?

好,請聽我娓娓道來。

到目前為止,RenderObject 看起來跟用 Native 的 View 很像,雖然可能還是沒有很懂為啥需要有個 Widget 和 Element。那回到最初的問題:為什麼 Flutter 的 Widget 可以像是不用錢一樣的,每次都給一個新的 instance 呢?上面的敘述只是稍微弄清了 Widget、Element、RenderObject 三者的關係,那具體到底怎麼辦到的呢?

PS: 嚴格說起來還是要點花費,只是成本相對低很多

來理解三者的關係吧

這邊簡單弄一個可以 set State 的 App

我在按下 TextButton 後,會觸發 setState ,經歷了很長一段在這邊不談的程式碼後,接著會在會重新呼叫到 build(BuildContext),然後拿去跟

updateChild 的時候呢,就會比對這個 Widget 是不是一樣(主要是比對 widget 的類型還有 key,而 key 絕大部分時候都是 null)?是不是能夠更新?或者是需要插入另一個新的 Widget 呢?

樹一層一層被遞迴來到了 RichText 的 Element 後,會呼叫 Widget 的 updateRenderObject

在這裡就決定了,甚麼東西需要重畫,甚麼東西不用(雖然把全部的 Property 全 assign 了一次了,但是每一個 property 都有判斷值相不相同,所以原則上這邊需要重畫的只有 text)。

有意思的地方來了,還記得前面說過,這邊的 Widget 是誰拿著的?那這邊的 RenderObject 又是誰拿著的。

沒錯,都是 Element 。

簡單來說,runApp 完後,大致上的局勢是這樣的。

在 setState 的時候, MyHomePage’s 的 StatefulElement 會得到一個新的 Widget。

那麼 Element 會往下丟給它的 Child。

這邊確定一樣後,就繼續往下丟給它的 Child。

這邊會問能否 update ,原則上兩個類型一樣就會 update 了,所以會送給 Element 的 RenderObject ,而 RenderObject 再比對有沒有 Property 不同,再去決定甚麼東西要重畫。

TextButton 也差不多,只是很顯然 TextButton 在此時沒有東西要重畫。

回過頭來改一下 Code :

我如果按了一下按鈕,會把原本的 TextButton 給取代為另一個新的 RichText 。

一開始 MyHomePage 得到新的一個 Column ,他把他往下給他的 Child — 原本的 Column:

對於 Column 的 Element 來說,他會發現,喔喔,我的 child 被換掉了,那舊的 child 我不要了,把 Element 連同 RenderObject 一起丟了,我需要重新 layout 來放新的 child。

這裡先略過做完更新原有 RichText 的步驟。新的 child 得產生新的 Element 。

新的 Element 需要新的 RenderObject ,於是就走了 inflateWidget 的流程,產生新的 RenderObject ,接著把 RichText 給畫出來了。

結語

說了這麼多,大概可以說明一下 ,Widget、Element、RenderObject 是甚麼了?

RenderObject 就是負責繪圖。說碰到它很昂貴有點沉重,基本上畫面有變就會動到他,它至少會去比對 Property 有沒有不一樣、要不要重繪。所以實際上還是很常碰到 RenderObject ,但在理論上在寫 Flutter 的時候不太容易直接碰到它就是了。

Element 可以當作是當前 Widget 還有 RenderObject 的狀態持有者。雖然官方文件說它是 Widget 的 instantiation,但它並不是 Widget widget = new Element(); 這種關係。Widget 的 instantiation 需要有一個 RenderObject 和自身 Widget 的設定,Element 負責這事情,所以 Element 是 widget 的 instantiation。那麼Flutter 是跨平台的框架,但是主力是在兩個手機雙平台上,雙平台基本上不可避免生命週期的東西,那 Element 他持有 Widget 和 RenderObject 的狀態,應該也可以猜到它會負責生命週期的東西,但不是這篇的重點,這邊就不贅述了。

Widget 理論上會是 Flutter 開發者最常碰到的東西。那他其實除了負責持有 RenderObject 應該要怎麼 layout 怎麼 render 的資訊外。此外他還會持有怎麼產生自己的 element ,且根據 element 而定(像 Statefulwidget,他是會另外產生 Widget ,所以他並不需要產生 RenderObject),他也會需要知道怎麼建立屬於他的 RenderObject 以及怎麼 updateRenderObject,這些事情都是由它產生的 Element 或是做為他的 parent 的 Element 去做的,包括產生自己的 Element ,instance widget 本身並不會做這些事情,所以,產生一個新的 widget instance 是很廉價,因為產生他只會生成資料不會直接生成 Element 和 RenderObject,昂貴與否得看你怎麼去變動你的 layout widget 那些。

知道這些後,就知道為什麼 Widget 可以像不用錢似的不斷重新 instance ,因為本質上他就是一個設定檔、或者說藍圖的存在。小生我對於 Web 前端並不太熟,但這個模式跟 DOM 好像類似就是了。

如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我寫文章最大的動力。

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.