Flutter原理:三棵重要的樹(渲染過程、佈局約束、應用視圖的構建等)

瞭解 HTML 的讀者一定聽說過 DOM 樹這個概念,它由頁面中每一個控件組成,這些控件所形成的一種天然的嵌套關係使其可以表示爲 “樹” 結構,我們也可以將這個概念應用在 Flutter 中,例如默認的計數器應用的結構如下圖:

我們也可以看到上圖中每個控件所形成的樹結構中隱含了一些關係,例如在上圖中,我們可以說 Text 組件是 Column 組件的子組件,Scaffold 是 AppBar 的父組件,這樣的層級關係使得每個控件都清晰的連接到了一起,樹結構由此而來。

我們也知道 Container、Text 等組件都屬於 Widget,所以在 Flutter 中我們將這種樹稱爲 Widget 樹,也可以叫做控件樹,它就表示了我們在 dart 代碼中所寫的控件的結構。


另外,在 Flutter 體系結構中,真正做組件渲染在屏幕上這個任務的並非在 控件層(Widget)層,而是在渲染(Rending)層,那麼我們在代碼中所寫組件又是怎麼通過渲染層顯示的呢?Flutter 中又引入了 Element 樹和 RenderingObject 樹兩棵樹。

Element 是什麼,我們可以把它稱之爲 Widget 另一種抽象。讀者也可以把它看作一個更爲實際控件,因爲在我們的手機屏幕上顯示的控件並非我們在代碼中所寫的 Widget,我們在代碼中所使用的像 Container、Text 等這類組件和其屬性只不過是我們想要構建的組件的配置信息,當我們第一次調用 build() 方法想要在屏幕上顯示這些組件時,Flutter 會根據這些信息生成該 Widget 控件對應的 Element,同樣地,Element 也會被放到相應的 Element 樹當中。在 Flutter 中,一個 Widget 通過多次複用可以對應多個 Element 實例,Element 纔是我們真正在屏幕上顯示的元素。

Element 與 Widget 另一個區別在於,Widget 天然是不可變的(immutable),它如要更新便需要重建,如果想要把可變狀態與 Widget 關聯起來,可以使用 StatefulWidget,StatefulWidget 通過使用StatefulWidget.createState 方法創建 State 對象,並將之擴充到 Element 以及合併到樹中;

這裏,爲了更爲深刻的理解以上描述的含義,我們可以舉一個更爲形象的例子。Widget 作爲大 Boss,他把近期的戰略部署,即配置信息,寫在紙上下發給經理人 Element,Element 看到詳細的配置信息開始真正的開起活來了。我們還需要注意一點,大 Boss 隨時會改變戰略部署,然後不會在原有的紙上修改而是重新寫下來,這時經理人爲了減少工作量需要將新的計劃與舊的計劃比較來作出相應的更新措施。這也是 Flutter 框架層做的一大優化。下面又來了,Element 作爲經理人也很體面,當然不會把活全乾完,於是又找了一個 RenderObject 的員工來幫它做粗重的累活。

RenderObject 在 Flutter 當中做組件佈局渲染的工作,其爲了組件間的渲染搭配及佈局約束也有對應的 RenderObject 樹,我們也稱之爲渲染樹。

熟悉了 Flutter 中的上述三顆樹,相信讀者會對組件的渲染過程有了一個清晰的認識,這對我們之後學習常用組件有很大的幫助,我們需要用不同的眼光去看待我們所建立的佈局和控件,之後我們也會更加深入的去理解其中更不爲人知的奧祕。

組件渲染過程簡述

從上文中,我們知道控件樹中的每個控件都會實現一個 RenderObject 對象做渲染任務,並將所有的RenderObject 組成渲染樹。Flutter 渲染組件的過程如下:

Flutter 的渲染過程由用戶的輸入開始,當接受到用戶輸入的信號時,就會觸發動畫的進度更新,例如我們第一次渲染時的啓動動畫,或者我們在滾動手機屏幕時單個列表項複用時的移動動畫。之後便需要開始視圖數據的構建(build),這一步中 Flutter 創建了前文所描述的三棵視圖樹。

在這之後,視圖纔會進行佈局(layout),計算各個部分的大小,然後進行繪製(paint),生成每個視圖的視覺數據,這部分的任務主要就是由 RenderObject 所做。這裏,Flutter 中的佈局過程可用下圖表示,在上述構建完成渲染樹後,父渲染對象會將佈局約束信息向下傳遞,子渲染對象根據自己的渲染情況返回 Size,Size 數據會向上傳遞,最終父渲染對象完成佈局過程。

最後一步進行“光柵化”(Rasterize),前一步得到合成的視圖數據其實還是一份矢量描述數據,光柵化幫助把這份數據真正地生成一個一個的像素填充數據。在 Flutter 中,光柵化這個步驟被放在了 Engine 層中。

在日常開發學習中,我們只需要在代碼層配置好我們的 Widget 樹,瞭解各種 Widget 特性及使用方法,其餘的工作都可以交給我們的框架層去實現。

簡述計數器應用的渲染過程

佔坑

元素樹詳解

我們已經知道了各類控件的作用及其使用方法,這些 Widget 被我們開發人員配置了多個屬性來定義它的展現形式,例如配置 Text 組件需要顯示的字符串,配置輸入框組件需要顯示的內容。我們 Element 樹會記錄這些配置信息。熟悉 React 的讀者可能瞭解過其中的 “虛擬 DOM” 這個概念,上述 Flutter 這種操作也正體現了這一概念。Widget 是不可變,它的改變就意味着要重建,而其重建也非常頻繁,如果我們將更多的任務都交給它將會對性能造成很大的損傷,因此我們把 Widget 組件當作一個虛擬的組件樹,而真正被渲染在屏幕上的其實是 Elememt 這棵樹,它持有其對應 Widget 的引用,如果他對應的 Widget 發生改變,它就會被標記爲 dirty Element,於是下一次更新視圖時根據這個狀態只更新被修改的內容,從而達到提升性能的效果。

每次,當控件掛載到控件樹上時,Flutter 調用其 createElement() 方法,創建其對應的 Element。Flutter 再將這個 Element 放到元素樹上,並持有創建它控件的引用,如下圖:

控件會有它的子樹:

子控件也會創建相應 Element 被放在元素樹上:

Element 中的狀態

我們上文提到了 Widget 的不可變性,相應的 Element 就有其可變性,正如我們前文所說的它被標記爲 dirty Element 便是作爲需要更新的狀態,另外一個我們需要格外注意的是,有狀態組件(statefulWidget)對應的 State 對象其實也被 Element 所管理,如下圖所示。

Flutter 中的 Widget 一直在重建,每次重建之後,Element 都會採用相應的措施來確定是否我對應的新控件跟之前引用舊控件是否有所改變,如果沒改變則只需要做更新操作,如果前後不同則會重創建。那麼,Element 根據什麼來確定控件是否改變呢?它會比較 Widget 以下兩個屬性:

  • 組件類型
  • Widget 的 Key (如果有)

組件類型即前後控件的是否是同一個類所創建的,Key 即爲每個控件的唯一標識。

例子證明 Elment 持有組件狀態

佔坑

渲染樹詳解

我們已經大致知道 Flutter 中的三棵重要的樹及 Element 樹的工作原理,其中第三棵渲染樹的任務就是做組件的具體的佈局渲染工作。

渲染樹上每個節點都是一個繼承自 RenderObject 類的對象,其由 Element 中的 renderObject 或 RenderObjectWidget 中的 createRenderObject 方法生成,該對象內部提供多個屬性及方法來幫助框架層中的組件如何佈局渲染。

我們知道 StatelessWidget 和 StatefulWidget 兩種直接繼承自 Widget 的類,在 Flutter 中,還有另一個類 RenderObjectWidget 也同樣直接繼承自 Widget,它沒有 build 方法,可通過 createRenderObject 直接創建 RenderObject 對象放入渲染樹中。Column 和 Row 等控件都間接繼承自RenderObjectWidget。

主要屬性和方法如下:

  • constraints 對象,從其父級傳遞給它的約束
  • parentData 對象,其父對象附加有用的信息。
  • performLayout 方法,計算此渲染對象的佈局。
  • paint 方法,繪製該組件及其子組件。

RenderObject 作爲一個抽象類。每個節點需要實現它才能進行實際渲染。擴展 RenderOject 的兩個最重要的類是RenderBox 和 RenderSliver。這兩個類分別是應用了 Box 協議和 Sliver 協議這兩種佈局協議的所有渲染對象的父類,其還擴展了數十個和其他幾個處理特定場景的類,並實現了渲染過程的細節,如 RenderShiftedBox 和 RenderStack 等等。

佈局約束

在上面,我們介紹組件渲染流程時,我們瞭解到了 Flutter 中的控件在屏幕上繪製渲染之前需要先進行佈局(layout)操作。其具體可分爲兩個線性過程:從頂部向下傳遞約束,從底部向上傳遞佈局信息,其過程可用下圖表示。

第一個線性過程用於傳遞佈局約束。父節點給每個子節點傳遞約束,這些約束是每個子節點在佈局階段必須要遵守的規則。就好像父母告訴自己的孩子 :“你必須遵守學校的規定,纔可以做其他的事”。常見的約束包括規定子節點最大最小寬度或者子節點最大最小的高度。這種約束會向下延伸,子組件也會產生約束傳遞給自己的孩子,一直到葉子結點。

第二的線性過程用來傳遞具體的佈局信息。子節點接受到來自父節點的約束後,會依據它產生自己具體的佈局信息,如父節點規定我的最小寬度是 500 的單位像素,子節點按照這個規則可能定義自己的寬度爲 500 個像素,或者低於 500 像素的任何一個值。這樣,確定好自己的佈局信息之後,將這些信息告訴父節點。父節點也會繼續此操作向上傳遞一直到最頂部。

下面我們具體介紹有哪些具體的佈局約束可在樹中傳遞。Flutter 中有兩種主要的佈局協議:Box 盒子協議和 Sliver 滑動協議。這裏我們先以盒子協議爲例展開具體的介紹。

在盒子協議中,父節點傳遞給其子節點的約束爲 BoxConstraints。該約束規定了允許每個子節點的最大和最小寬度和高度。如下圖,父節點傳入 Min Width 爲 150,Max Width 爲 300 的 BoxConstraints:

當子節點接受到該約束,便可以取得上圖中綠色範圍內的值,即寬度在 150 到 300 之間,高度大於 100,當取得具體的值之後再將取得具體的大小的值上傳給父節點,從而達到父子的佈局通信。

自定義一個 Center 控件

之後更新,大家也可以看各組件的源碼探究其如何應用上面提到的原理。

應用視圖的構建

Flutter App 入口的部分發生於如下代碼:

import 'package:flutter/material.dart';

// 這裏的 MyApp是一個 Widget
void main() => runApp(new MyApp());

runApp函數接受一個 Widget類型的對象作爲參數,也就是說在 Flutter的概念中,只存在 View,而其他的任何邏輯都只爲 View的數據、狀態改變服務,不存在 ViewController(或者叫 Activity)。
接下來看 runApp做了什麼:

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      new WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

runApp 中,傳入的 widget 被掛載到根 widget 上。這個 WidgetsFlutterBinding 其實是一個單例,通過 mixin 來使用框架中實現的其他 binding 的 Service,比如手勢、基礎服務、隊列、繪圖等等。然後會調用 scheduleWarmUpFrame 這個方法,從這個方法註釋可知,調用這個方法會主動構建視圖數據。這樣做的好處是因爲 Flutter 依賴 Dart 的 MicroTask 來進行幀數據構建任務的 schedule,這裏通過主動調用進行整個週期的 “熱身”,這樣最近的下次 VSync 信號同步時就有視圖數據可提供,而不用等到 MicroTask 的 next Tick。

然後我們再來看 attachRootWidget 這個函數幹了什麼:

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
}

attachRootWidget 把 widget交給了 RenderObjectToWidgetAdapter這座橋樑,通過這座橋樑,Element 被創建,並且同時能持有 Widget 和 RenderObject的引用。然後我們從上文就知道後面發生的就是第一次的視圖數據構建了。

從這一部分能印證了:Flutter應用通過 Widget、Element、RenderObject 三種樹結構來維護整個應用的視圖數據。

附言

在沒更新文章的這段期間一直在準備春招,原本就準備寫一些關於 Flutter 原理的文章,今天發現已經有不少大佬在解析源碼,尤其看到了 戀貓de小郭 的文章寫得很好,希望我的一些總結也能幫助到大家吧!

我的博客:https://meandni.com/2019/05/05/flutter-principle/

我的Github:https://github.com/MeandNi/

歡迎一起討論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章