Flutter中的層級蛋糕

reply-1988發表於2019-01-14

Flutter是如何使用Widgets、Elements和RenderObjects來實現如此令人驚豔的視覺效果的呢?

本文已經得到作者的允許,將其原文The Layer Cake翻譯成中文。鑑於本人的英語能力以及表達能力有限,請英語水平足夠的朋友前往原文地址去閱讀=。=。

delicious cake

Flutter是一個優秀的UI框架,它能夠幫助我們快速的構建出漂亮的使用者介面。只需要很少的程式碼和熱過載,你的APP就能夠擁有高達120fps的流暢性。但是,你是否想過Flutter是如何做到這一切的呢?Flutter使用了什麼樣的魔法來實現這一切的呢?或者說Flutter內部是如何工作的呢?我們將會探索這一切,請拿杯茶或者咖啡然後繼續閱讀下去吧。也許你已經聽過Flutter中一切皆為Widget。你的APP是個Widget、Text是個WidgetWidget周圍的padding也是Widget,甚至recognise gestures(手勢識別)也是一個Widget。但是這些並不是全部的事實。如果我告訴你Widget的確很棒,能夠幫助你快速的構建出APP,但是我不使用任何一個Widget就能夠完成App的構建你相信嗎?讓我們先來深入框架來看看如何做到這一切吧。####The Four Layers也許你已經在一些類似於‘Flutter入門介紹’的文章中對Flutter有了比較大致的瞭解。但是你並沒有能夠真正的理解這些層級所代表的概念。也許你像我一樣看著這張圖看了20s卻不知道怎麼理解。不用擔心,我會幫助你的。看下下面的這個圖吧。

four layers

Flutter framework由許多抽象的層級組成。在這些層級的最頂端是我們經常用到的MaterialCupertino Widgets。我們大多數情況下使用的就是這兩類Widget。在Widget層下面,你會發現Rendering層。Rendering層簡化了佈局和繪製過程。它是dart:ui的的抽象化。dart:ui是框架的最底層,它負責處理與Engine層的交流溝通。簡而言之,等級越高的層越容易使用,但是等級越低的層,暴露出來的api越多,越能夠增加自定義功能。####1. The dart:ui library

dart:ui library暴露出最底層的服務,這些服務被用來引導Application,例如用來驅動輸入、繪製文字、佈局和渲染子系統。

所以你可以僅僅通過使用例項化dart:ui庫中的類(例如CanvasPaintTextField)來構建一個Flutter App。但是如果你對於直接在canvas上繪製比較熟悉,就會知道使用這些底層api繪製一個圖案是既難又繁瑣的。接下來考慮一些不是繪製的東西吧,例如佈局和命中測試。這些意味著什麼呢?這意味著你必須手動的計算所有在你佈局中使用的座標。然後混合一些繪製和命中測試來捕獲使用者的輸入。對每一幀進行上述操作並追蹤它們。這個方法對於那些比較簡單的APP,比如一個在藍色區域內展示文字這種比較適用。如果對於那些比較複雜的APP或者簡單的遊戲來說可夠你受的了。更不用說產品經理最喜愛的動畫、滾動和一些酷炫的UI效果了。用我多年的開發經驗告訴你,這些是開發者無窮無盡的夢魘。####2. The Rendering library

Flutter的Rendering tree(渲染樹)。RenderObject的層級結構被Flutter Widgets庫使用來實現其佈局和後臺的繪製。通常來說,儘管你可能會使用RenderBox來在你的應用中實現自定義的效果,但是大多數情況下我們唯一與RenderObject的互動就是在除錯佈局資訊的時候。

Rendering librarydart: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之一。

  1. Layout 例如ColumnRow Widgets用來幫助我們輕鬆的處理其他Widget的佈局。
  2. Painting 例如TextImage Widgets允許我們展示(繪製)一些內容在螢幕上。
  3. Hit-Testing 例如GestureDetector允許我們識別出不同的手勢,例如點選和滑動。

大多數情況下我們會使用一些“基礎”Widget來組成我們需要的Widget。例如我們使用GestureDetec來包裹ContainerContainer中包裹Button來處理按鈕點選。這叫做組合而不是繼承。然而除了自己構建每個UI元件,Flutter團隊還建立了兩個包含常用的MaterialCupertino風格的Widgets的庫。4. The Material &
Cupertino library

使用Material和Cupertino設計規範的Widgets庫。

Flutter是一切皆抽象,盡一切可能來減輕開發者的負擔。這是第四等級的包含預先構建好的MaterialCupertino風格的Widgets層。

####Put it all TogetherRenderObject是如何與Widgets連線起來的呢?Flutter是如何建立佈局?Element又是什麼呢?已經說的夠多了,讓我們在實踐中學習吧。考慮如下Widgets樹。

在現實世界中,類似於Text這種Widget是由其他一些Widgets組成而來的,為了簡化這些,我們引用phantasmal SimpleContainer和SimpleText。

我們構建的這個APP是非常簡單的。它由三個Stateless Widget組成:SimpleAppSimpleContainerSimpleText。所以如果我們呼叫Flutter的runApp()方法會發生什麼呢?當runApp()被呼叫時,第一時間會在後臺發生以下事件。

  1. Flutter會構建包含這三個Widget的Widgets樹。
  2. Flutter遍歷Widget樹,然後根據其中的Widget呼叫createElement()來建立相應的Element物件,最後將這些物件組建成Element樹。
  3. 第三個樹被建立,這個樹中包含了與Widget對應的Element通過createRenderObject()建立的RenderObject。

下圖是Flutter經過這三個步驟後的狀態:
Flutter中的層級蛋糕

Flutter建立了三個不同的樹,一個對應著Widget,一個對應著Element,一個對應著RenderObject每一個Element中都有著相對應的WidgetRenderObject的引用。

那什麼又是RenderObject呢?RenderObject中包含了所有用來渲染例項Widget的邏輯。它負責layoutpaintinghit-testing。它的生成十分耗費效能,所以我們應該儘可能的快取它。我們把它在記憶體中儘可能的儲存更長的時間,甚至回收利用它們(因為它們的例項化真的很耗費資源)。這個時候Element就登場了。Element是存在於可變Widget樹和不可變RenderObject樹之間的橋樑。Element擅長比較兩個Object,在Flutter裡面就是WidgetRenderObject。它的作用是配置好Widget在樹中的位置,並且保持對於相對應的RenderObjectWidget的引用。為什麼使用三個樹而不是一個樹呢?簡而言之是為了效能。當Widget樹改變的時候,Flutter使用Element樹來比較新的Widget樹和原來的RenderObject樹。如果某一個位置的WidgetRenderObject型別不一致,才需要重新建立RenderObject。如果其他位置的WidgetRenderObject型別一致,則只需要修改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樹中第二個WidgetRenderObject樹中第二個RenderObject的型別,以此類推,直到Widget樹和RendObject樹比較完成。

改變Container的顏色

Flutter遵循一個最基本的原則:判斷新的Widget和老的Widget是否是同一個型別。如果不是同一個型別,那就把WidgetElementRenderObject分別從它們的樹(包括它們的子樹)上移除,然後建立新的物件。如果是一個型別,那就僅僅修改RenderObject中的配置,然後繼續向下遍歷。在我們的例子中,SimpleApp Widget是和原來一樣的型別,它的配置也是和原來的SimpleAppRender一樣的,所以什麼都不會發生。下一個item在Widget樹中是SimpleContainer Widget,它的型別和原來是一樣的,但是它的顏色變化了,RenderObject的配置發生變化了。因為SimpleObject仍然需要一個SimpleContainerRender來渲染,Flutter只是更新了SimpleContainerRender的顏色屬性,然後要求它重新渲染。其他的物件都保持不變。

注意這三個樹,配置發生改變之後,Element和RenderObject例項沒有發生變化。

這個過程是非常快的,因為Flutter非常擅長建立那些輕量級的Widgets。那些重量級的RenderObject則是保持不變,直到與其相對應型別的WidgetWidget樹中被移除。那如果Widget的型別發生改變了會發生什麼呢?

SimpleText被SimpButton替代

和原來一樣,Flutter會對Widget樹的頂端向下遍歷,與RenderObject樹中的RenderObject型別進行對比。

新的Widget樹,SimpleText Widget和與之對應的Element、RenderObject都從其樹上消失了。

因為SimpleButton的型別與Element樹中相對應位置的Element的型別不同(實際上還是與RenderObject的型別進行比較),Flutter將會從各自的樹上刪除這個Element和相對應的SimpleTextRender。然後Flutter將會重建與SimpleButton相對應的ElementRenderObject

最終的樹

然後完成了,新的RenderObject樹已經被重建,並將會計算佈局,然後繪製在螢幕上面。Flutter內部使用了很多優化方法和快取策略來處理,所以你不需要手動處理這個。####Conclusion現在你應該對Flutter為什麼能以如此快的速度渲染複雜佈局有了大致的瞭解了。我希望這篇文章能夠幫助你更好的理解Flutter內部的設計理念。我的Twitter是 Frederik Schweiger,期待與你的交流。

來源:https://juejin.im/post/5c3c7759f265da6143134797#comment

相關文章