[譯]Flutter中的層級結構

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殊不知道怎麼理解。不用擔憂,我會幫助你的。看下下面的這個圖吧。 canvas

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,例如用來驅動輸入、繪製文字、佈局和渲染子系統。api

因此你能夠僅僅經過使用實例化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改變了,只須要裝載其的盒被系統從新計算便可。less

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 Together

RenderObject是如何與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建立了三個不一樣的樹,一個對應着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,期待與你的交流。

相關文章
相關標籤/搜索