Flutter是如何使用Widgets、Elements和RenderObjects來實現如此令人驚豔的視覺效果的呢?
本文已經得到作者的允許,將其原文The Layer Cake翻譯成中文。鑑於本人的英語能力以及表達能力有限,請英語水平足夠的朋友前往原文地址去閱讀=。=。
Flutter是一個優秀的UI框架,它能夠幫助我們快速的構建出漂亮的使用者介面。只需要很少的程式碼和熱過載,你的APP就能夠擁有高達120fps的流暢性。但是,你是否想過Flutter是如何做到這一切的呢?Flutter使用了什麼樣的魔法來實現這一切的呢?或者說Flutter內部是如何工作的呢?我們將會探索這一切,請拿杯茶或者咖啡然後繼續閱讀下去吧。也許你已經聽過Flutter中一切皆為Widget
。你的APP是個Widget
、Text是個Widget
,Widget
周圍的padding
也是Widget
,甚至recognise gestures
(手勢識別)也是一個Widget
。但是這些並不是全部的事實。如果我告訴你Widget
的確很棒,能夠幫助你快速的構建出APP,但是我不使用任何一個Widget
就能夠完成App的構建你相信嗎?讓我們先來深入框架來看看如何做到這一切吧。####The Four Layers也許你已經在一些類似於‘Flutter入門介紹’的文章中對Flutter有了比較大致的瞭解。但是你並沒有能夠真正的理解這些層級所代表的概念。也許你像我一樣看著這張圖看了20s卻不知道怎麼理解。不用擔心,我會幫助你的。看下下面的這個圖吧。
Flutter framework由許多抽象的層級組成。在這些層級的最頂端是我們經常用到的Material
和Cupertino
Widgets。我們大多數情況下使用的就是這兩類Widget。在Widget層下面,你會發現Rendering
層。Rendering
層簡化了佈局和繪製過程。它是dart:ui
的的抽象化。dart:ui
是框架的最底層,它負責處理與Engine
層的交流溝通。簡而言之,等級越高的層越容易使用,但是等級越低的層,暴露出來的api越多,越能夠增加自定義功能。####1. The dart:ui library
dart:ui library
暴露出最底層的服務,這些服務被用來引導Application,例如用來驅動輸入、繪製文字、佈局和渲染子系統。
所以你可以僅僅通過使用例項化dart:ui
庫中的類(例如Canvas
、Paint
和TextField
)來構建一個Flutter App。但是如果你對於直接在canvas
上繪製比較熟悉,就會知道使用這些底層api繪製一個圖案是既難又繁瑣的。接下來考慮一些不是繪製的東西吧,例如佈局和命中測試。這些意味著什麼呢?這意味著你必須手動的計算所有在你佈局中使用的座標。然後混合一些繪製和命中測試來捕獲使用者的輸入。對每一幀進行上述操作並追蹤它們。這個方法對於那些比較簡單的APP,比如一個在藍色區域內展示文字這種比較適用。如果對於那些比較複雜的APP或者簡單的遊戲來說可夠你受的了。更不用說產品經理最喜愛的動畫、滾動和一些酷炫的UI效果了。用我多年的開發經驗告訴你,這些是開發者無窮無盡的夢魘。####2. The Rendering library
Flutter的
Rendering tree
(渲染樹)。RenderObject
的層級結構被Flutter Widgets庫使用來實現其佈局和後臺的繪製。通常來說,儘管你可能會使用RenderBox
來在你的應用中實現自定義的效果,但是大多數情況下我們唯一與RenderObject
的互動就是在除錯佈局資訊的時候。
Rendering library
是dart:ui library
上第一個抽象層。它替你做了所有繁重的數學計算工作(例如跟蹤需要不斷計算的座標)。它使用RenderObjects
來處理這些工作。你可以把RenderObjects
想象成一個汽車的發動機,它承擔了所有把你的APP展示到螢幕的工作。Rendering tree
中的所有RenderObjects
都會被Flutter分層和繪製。為了優化這個複雜的過程,Flutter使用了一個智慧演算法來快取這些例項化很耗費效能的物件從而實現在效能最優化。大多數情況,你會發現Flutter使用RenderBox
而不是RenderObject
。這是因為專案的構建者發現使用一個簡單和盒佈局約束就能夠成功的構建出有效穩定的UI。想象一下所有的Widget都被放置在它們的盒中。這個盒中的相關引數都計算好了,然後被放置到其他已經整理好的盒中間。所以如果在你的佈局中僅有一個Widget改變了,只需要裝載其的盒被系統重新計算即可。3. The Widget library
Flutter Widgets框架
Widget庫或許是最有意思的庫。它是另外一個用來提供開箱即用的Widget的抽象層。這個庫中所有的Widget都屬於以下三種使用適當的RenderObject
處理的Widget之一。
Layout
例如Column
和Row
Widgets用來幫助我們輕鬆的處理其他Widget的佈局。Painting
例如Text
和Image
Widgets允許我們展示(繪製)一些內容在螢幕上。Hit-Testing
例如GestureDetector
允許我們識別出不同的手勢,例如點選和滑動。
大多數情況下我們會使用一些“基礎”Widget來組成我們需要的Widget。例如我們使用GestureDetec
來包裹Container
,Container
中包裹Button來處理按鈕點選。這叫做組合而不是繼承。然而除了自己構建每個UI元件,Flutter團隊還建立了兩個包含常用的Material
和Cupertino
風格的Widgets的庫。4. The Material &
Cupertino library
使用Material和Cupertino設計規範的Widgets庫。
Flutter是一切皆抽象,盡一切可能來減輕開發者的負擔。這是第四等級的包含預先構建好的Material
和Cupertino
風格的Widgets層。
####Put it all TogetherRenderObject
是如何與Widgets
連線起來的呢?Flutter是如何建立佈局?Element又是什麼呢?已經說的夠多了,讓我們在實踐中學習吧。考慮如下Widgets樹。
我們構建的這個APP是非常簡單的。它由三個Stateless Widget
組成:SimpleApp
、SimpleContainer
、SimpleText
。所以如果我們呼叫Flutter的runApp()方法會發生什麼呢?當runApp()
被呼叫時,第一時間會在後臺發生以下事件。
- Flutter會構建包含這三個Widget的Widgets樹。
- Flutter遍歷Widget樹,然後根據其中的Widget呼叫
createElement()
來建立相應的Element物件,最後將這些物件組建成Element
樹。 - 第三個樹被建立,這個樹中包含了與Widget對應的
Element
通過createRenderObject()
建立的RenderObject。
下圖是Flutter經過這三個步驟後的狀態:
Flutter建立了三個不同的樹,一個對應著Widget
,一個對應著Element
,一個對應著RenderObject
。每一個Element
中都有著相對應的Widget
和RenderObject
的引用。
那什麼又是RenderObject
呢?RenderObject
中包含了所有用來渲染例項Widget
的邏輯。它負責layout
、painting
和hit-testing
。它的生成十分耗費效能,所以我們應該儘可能的快取它。我們把它在記憶體中儘可能的儲存更長的時間,甚至回收利用它們(因為它們的例項化真的很耗費資源)。這個時候Element
就登場了。Element
是存在於可變Widget
樹和不可變RenderObject
樹之間的橋樑。Element
擅長比較兩個Object
,在Flutter裡面就是Widget
和RenderObject
。它的作用是配置好Widget
在樹中的位置,並且保持對於相對應的RenderObject
和Widget
的引用。為什麼使用三個樹而不是一個樹呢?簡而言之是為了效能。當Widget
樹改變的時候,Flutter使用Element
樹來比較新的Widget
樹和原來的RenderObject
樹。如果某一個位置的Widget
和RenderObject
型別不一致,才需要重新建立RenderObject
。如果其他位置的Widget
和RenderObject
型別一致,則只需要修改RenderObject
的配置,不用進行耗費效能的RenderObject
的例項化工作了。因為Widget
是非常輕量級的,例項化耗費的效能很少,所以它是描述APP的狀態(也就是configuration)的最好工具。重量級的RenderObject
(建立十分耗費效能)則需要儘可能少的建立,並儘可能的複用。就像Simon所說:整個Flutter APP就像是一個RecycleView。然而,在框架中,Element
是被抽離開來的,所以你不需要經常和它們打交道。每個Widget的build(BuildContext context)
方法中傳遞的context
就是實現了BuildContext
介面的Element
,這也就是為什麼相同類別的單個Widget
不同的原因。####Computer the Next Frame因為Widget
是不可變的,當某個Widget
的配置改變的時候,整個Widget
樹都需要被重建。例如當我們改變一個Container
的顏色為紅色的時候,框架就會觸發一個重建整個Widget
樹的動作。然後在Element
的幫助下,Flutter比較新的Widget
樹中的第一個Widget
型別和RenderObject
樹中第一個RenderObject
的型別。接下來比較Widget
樹中第二個Widget
和RenderObject
樹中第二個RenderObject
的型別,以此類推,直到Widget
樹和RendObject
樹比較完成。
Flutter遵循一個最基本的原則:判斷新的Widget
和老的Widget
是否是同一個型別。如果不是同一個型別,那就把Widget
、Element
、RenderObject
分別從它們的樹(包括它們的子樹)上移除,然後建立新的物件。如果是一個型別,那就僅僅修改RenderObject
中的配置,然後繼續向下遍歷。在我們的例子中,SimpleApp Widget
是和原來一樣的型別,它的配置也是和原來的SimpleAppRender
一樣的,所以什麼都不會發生。下一個item在Widget樹中是SimpleContainer Widget
,它的型別和原來是一樣的,但是它的顏色變化了,RenderObject
的配置發生變化了。因為SimpleObject
仍然需要一個SimpleContainerRender
來渲染,Flutter只是更新了SimpleContainerRender
的顏色屬性,然後要求它重新渲染。其他的物件都保持不變。
這個過程是非常快的,因為Flutter非常擅長建立那些輕量級的Widgets
。那些重量級的RenderObject
則是保持不變,直到與其相對應型別的Widget
從Widget
樹中被移除。那如果Widget的型別發生改變了會發生什麼呢?
和原來一樣,Flutter會對Widget樹的頂端向下遍歷,與RenderObject
樹中的RenderObject
型別進行對比。
因為SimpleButton
的型別與Element
樹中相對應位置的Element
的型別不同(實際上還是與RenderObject
的型別進行比較),Flutter將會從各自的樹上刪除這個Element
和相對應的SimpleTextRender
。然後Flutter將會重建與SimpleButton
相對應的Element
和RenderObject
。
然後完成了,新的RenderObject
樹已經被重建,並將會計算佈局,然後繪製在螢幕上面。Flutter內部使用了很多優化方法和快取策略來處理,所以你不需要手動處理這個。####Conclusion現在你應該對Flutter為什麼能以如此快的速度渲染複雜佈局有了大致的瞭解了。我希望這篇文章能夠幫助你更好的理解Flutter內部的設計理念。我的Twitter是 Frederik Schweiger,期待與你的交流。