Flutter 內部是如何工作的?Widgets
、Elements
、BuildContext
到底是什么東西?為什么 Flutter 可以運行那么快?為什么有時候運行的效果并不符合我們的預期?什么是所謂視圖樹?——本文將一一為你解答。
以下為譯文:Flutter 內部是如何工作的?Widgets
、Elements
、BuildContext
、RenderObject
這些都是些什么東西?
去年,我剛開始使用 Flutter 開發應用程序時,在互聯網上,只能找到極少量Flutter相關文檔。雖然有一些文章在談論 Flutter,但基本沒有文章寫 Flutter 內部的工作原理。
Widgets
、Elements
、BuildContext
到底是什么東西?為什么 Flutter 可以運行那么快?為什么有時候運行的效果并不符合我們的預期?什么是所謂視圖樹?
在Flutter開發一個應用程序時,有 95% 的需求,都只需要處理 Widgets
,用它來展示 UI 并處理屏幕交互。但你是否考慮過,整個系統是如何工作的,是如何知道要更新哪些 UI 呢?
這一部份內容包含一些關鍵概念,了解他們,會有助于理解后面的內容。
硬件
讓我們從最基礎的東西開始說起。
當你在使用你的設備的時候,或者說你在使用某個應用程序的時候,你只會看到一塊屏幕。
事實上,在屏幕上,你看到了一系列的像素點,這些像素點,共同組成了一個二維的圖像。當你觸摸屏幕的時候,屏幕也只識別你手指在屏幕中的位置。
神奇的是,我們的應用程序可以觸發屏幕顯示的圖像更新。在以下情況下,應用程序可以觸發屏幕顯示圖像更新:
將圖片渲染到屏幕上是由顯示屏硬件來保證,這些顯示屏都是按固定的間隔時間來刷新屏幕,通常是1秒鐘刷新60次。這個刷新的頻率,我們通常叫作 刷新率,用 HZ 表示。
設備從 GPU 接收到要在屏幕上顯示的數據,渲染顯示在屏幕上(注:GPU 是一種專用的電子電路,經過優化,可以從 polygons 和 textures 獲取數據,并快速生成圖像)。我們將 GPU 在每秒中生成并發送給設備用于顯示的圖像的次數叫做 幀率, 使用 FPS 來做為單位進行計量。
看到這里,你也許會問,在文章的開篇,我們寫到二維圖像與硬件屏幕的渲染,這些又和 Flutter 的 Widgets 有什么關系呢?
答案很簡單,因為 Flutter 的一個核心功能就是合成二維圖像并處理交互。我認為,從這個角度來解讀,可以更好的理解 Flutter 的內部工作原理。
當然,還有一個原因,不管你信或者不信,Flutter 中幾乎所有的事情都是由刷新屏幕的需求來驅動的,我們需要在適當的時候來快速刷新我們的屏幕。
接口設計
不管什么時候,只要你加入Flutter開發的行列中,都會看到下面這張 Flutter 的架構圖。
我們在開發 Flutter 的應用程序的時候,使用 Dart 語言,我們面象的 API 都是 Flutter框架(綠色部分) 這一層提供的。
Flutter 框架通過 Window 這個抽象層與 Flutter引擎(藍色部分)進行通信, Window 中抽象出來了一系列的 API,實現了與硬件通信的接口。
當然,在下面的這些情況中,Flutter引擎也可以通過 Window 來通知 Flutter 框架層來進行事件處理。
Flutter Engine 渲染驅動 Flutter 框架
這一部分內容很難以理解,但是這就是真實的邏輯。
除了以下幾種情況, Flutter 框架的代碼執行都是由 Flutter 引擎觸發的。
注:一般情況下,如果Flutter渲染引擎沒有發出通知, Flutter 框架是不能更新任何UI的。有些時候,在沒有 Flutter 渲染引擎通知的情況下,也可以讓 Flutter 框架更新UI,但是并不建議這么做。
你或許會問我,執行手勢相關的邏輯,會讓 UI 發生變化;使用一個異步任務,或者動畫,也會讓 UI 發生改變,那它們又是如何工作的呢?
如果你想更新UI, 或者說你想在后臺執行代碼邏輯并更新 UI,你需要告訴 Flutter 引擎,這里有一些更改需要被渲染到屏幕上。通常情況下,在屏幕下一次刷新的時候, Flutter引擎會通知 Flutter框架,讓它來提供新場景的圖像來進行渲染顯示。
因此,Flutter 引擎是如何基于渲染編排整個應用程序的行為?從下面的動畫中,我們可以了解到整個內部運行機制:
整個動畫過程解釋如下:
RenderView 和 RenderObject
在討論與事件流相關的細節之前,先來看看視圖樹。
如之前所說, 所有東西最后都會轉換成像素顯示在屏幕中, Flutter 框將我們用于開發程序使用的 Widgets 轉化成可視部分,顯示在屏幕上。
在 Flutter 中,用來與渲染在屏幕上的可見視圖一一對應的對象,我們稱作 RenderObject
,它被用來表示:
定義屏幕中的區域,包括 大小,位置, 幾何結構。也可稱其為"渲染內容"。
識別可能受到手勢影響的屏幕區域。
一堆 RenderObject
共同組成了一棵樹,稱之為視圖樹。在視圖樹的最上面,也就是其跟節點,就是RenderView。RenderView
代表了整個輸出的視圖樹,它也是一種特殊的 Renderobject
, 如圖所示:
在文章的后面會講解 Widgets
與 RenderObjects
之前的關系。但在這之前,我們需要更深入的了解 RenderObjects
。
Bindings的初始化
當 Flutter 應用程序啟動的時候,系統會執行 main()
方法,它會調用 runApp(Widget app)
。
在調用runApp()這個方法的時候,Flutter 會初始化 Flutter 框架 與 Flutter 引擎 之間的接口,它被稱作是 bindings。
Bindings簡介
Bindings 是建立 Flutter 框架 與 Flutter 引擎之間通信的橋梁,Flutter的這兩個部分只能通過它傳遞數據(其中RenderView 是個例外,在后面會講到)。
每個bindings負責處理一組特定的任務、操作或者事件。本文中,作者按其作用域進行了重新分組。
到目前為止, Flutter 框架 提供8個 bindings,本文中,只討論以下四個:
為了保證完整性,我列出剩下四個 bindings:
當然,我也注意到了 WidgetsFlutterBinding ,但這個不是真正的 bindings, 而是一種 bindings 初始化工具。
有關 bindings 與 Flutter 引擎的交互邏輯,見下圖所示:
下面,我們分別來看一下這幾個 bindings。
SchedulerBinding
它有兩個主要功能:
當SchedulerBinding接受到喚醒事件 的時候,要做些什么呢?
舉個例子,假設您有一個動畫,并且已經開始執行了。這個動畫是由Ticker進行控制的,它需要以固定時間間隔觸發回調。要讓這樣的回調運行,我們需要告訴Flutter 引擎在下次刷新時喚醒我們(發送Begin Frame),觸發回調,執行動畫任務。在該動畫任務結束時,動畫還需要繼續,它將再次調用SchedulerBinding來調度另一幀。
當你響應導致視覺變化的事件(例如,更新屏幕的一部分的顏色,滾動,向屏幕中添加/從屏幕中刪除某些內容)時,我們需要采取必要的步驟來保證它可以正常的顯示在屏幕上。在這種情況下,Flutter框架將調用SchedulerBinding來告訴Flutter Engine去調度另一幀。
GestureBinding
這個 binding 處理手勢事件,并與 Flutter 引擎進行通信。它負責接受與手指有關的數據,并確定屏幕的哪一部分受到手勢的影響。然后,它會通知這些部分,來響應事件。
RendererBinding
它是 Flutter 引擎與視圖樹之前的橋梁,它有兩個不同的功能:
為了提供要在屏幕上呈現的修改,它負責驅動PipelineOwner并初始化RenderView。
PipelineOwner是一種協調器,它知道哪個RenderObject需要做一些與布局有關的事情并協調這些動作。
WidgetsBinding它用于監聽用戶設置的更改,如語言的修改。不僅如此, WidgetsBinding 否是 Widgets 與 Flutter 引擎之間通信的橋梁,有兩個主要的功能:
一些小組件的結構更改是 BuildOwner 來完成的,它跟蹤需要重建的小部件,并處理應用于整個小部件結構的其他任務。
基礎的內部原理講解完成,下面我們來看看 Widgets。
在所有有關 Flutter 的文檔中,你能看到這樣子的描述:在Fluter中,所有的對象都是 Widgets 。這樣子說雖然沒錯,但要說得更精確一些,我覺得應該是:
從開發人員的角度來看,在布局和交互方面,與用戶界面相關的所有內容均通過Widgets完成。
為啥要如此精確?Widget允許開發人員根據尺寸,內容,布局和交互性來定義屏幕的一部分。不僅這些,還有更多的東西也是通過它來定義。那么,什么是Widgets呢?
不變的屬性
在讀 Flutter 的源碼的時候,你可以在 Widget 類中看到如下定義:
1@immutable
2abstract class Widget extends DiagnosticableTree {
3 const Widget({ this.key });
4
5 final Key key;
6
7 ...
8}
這是什么意思?
@immutable 這個注解告訴我們,在 Widget 類中定義的所有變量都是 FINAL 的。換句話說,它們只能被定義一次。因此,一但初始化完成,這個 Widget 的內部變量將不能被修改。
Widgets 結構
當你在使用用Flutter的時候,你寫一個頁面,會用到 Widget,像下面的代碼中那樣,定義出要顯示的UI:
1Widget build(BuildContext context){
2 return SafeArea(
3 child: Scaffold(
4 appBar: AppBar(
5 title: Text('My title'),
6 ),
7 body: Container(
8 child: Center(
9 child: Text('Centered Text'),
10 ),
11 ),
12 ),
13 );
14}
上述例子中,一共使用了7個 Widgets,它們組成了一個樹狀結構。一個非常簡單的結構,根據代碼,可以畫出如下結構圖:
正如你看到的,他像一個樹, SafeArea 是這個樹的根節點。
復雜的 Widget 結構
正如你知道的那樣, Widget 可以將很多其它的Widget 聚合在一起。舉個例子,我可以使用如下代碼替換上面的代碼:
1Widget build(BuildContext context){
2 return MyOwnWidget();
3}
我們假設 MyOwnWidget
會自己去渲染 SafeArea, Scaffold 等 Widget。這個例子,要表達的意思是:
Widget 可能是一個頁子節點,也可能是一顆樹。
元素
為什么我會提到這個,正如我們將在后面看到的那樣,為了能夠生成可以在設備上渲染的圖像的像素,Flutter需要詳細了解組成屏幕的所有Widget,并確定所有部分, 按要求生成 Widget 。
為了說明這一點,你可以想象一下俄羅斯套娃,最開始的時候,你只能看到一個玩偶,但是這個玩偶中,包含了另一個,然后依次包含另一個,以此內推。
Flutter 生成所有的 Widgets 時,就像獲得所有的俄羅斯套娃一樣。
下圖展示了如何顯示 Widget 的所有部分,圖中,黃色部分表示你在代碼中寫到的部分,你可以在不同的組件中使用:
注:在這里我們使用了 "Widget 樹",這只是為了更好的理解邏輯,在 Flutter中并沒有這個概念。
現在我們來介紹前面提到的元素。
每個小部件都對應一個元素。元素彼此鏈接并形成一棵樹。因此,元素是樹中某個節點的引用。
首先,將元素作為一個節點,它有父結點,也有子結點。然后通過父子關系將它們鏈接在一起,就可以得到一個樹形結構。
如圖中看到的, 元素可以對應一個 Widget ,也可以對應一個 RenderObject。
總結一下:
Elements 定義了視圖部分的鏈接關系。
為了可以更好的表達 element 的概念,讓我們來看看下圖:
圖中所示, 元素樹連接了 Widgets 和 RenderObjects。
但是,為什么 Widget 會創建 Element?
三種 Widgets
在 Flutter , Widgets 可以拆分成三類(僅僅是我個人為了組織它們,而進行的分類),如下:
這類 Widget 主要作用是用來保存一些數據信息的,并且做為樹結構的根結點。點型的例子是 InheritedWidget
和 LayoutId
這些 Widgets 不會展現出任何的用戶頁面,但是它們會用來為其它的Widgets提供數據。
這類 Widget 直接或間接的用于屏幕布局:
典型例子:Row
,Column
,Stack
、Padding
、Align
、Opacity
、RawImage
等。
剩下的這一類 Widget 不能直接用于大小、位置、布局等設置,它們只是用來展示最終的數據信息。這些 Widget 通常被稱之為組件。
典型例子:RaisedButton
, Scaffold
, Text
, GestureDetector
, Container。
Widgets 分類
為什么拆分顯得如此重要?因為根據 Widget 的分類,會關聯不同的 Element。
Element 分類
下圖中展示了不同的 Element 分類:
內部Element 分類
Element 主要分為兩大類:
到目前為止,出現了很多的概念,它們是如何關聯在一起?
Widgets 和 Elements 是如何在一起工作的?
在Flutter中,整個系統都依賴 Widget / RenderObject 的狀態。
更新 element 的狀態的兩種不同方式:
Elements 的狀態變更是需要Elements的引用放入了臟元素列表中。使RenderObject無效意味著沒有任何更改應用于元素的結構,但是發生了renderObject級別的修改:
RenderObject 的狀態變更是需要將 RenderObject 的引用放在重繪/重建列表中。
不管是什么級別的變更,只要有變更發生,SchedulerBinding 就會發送一個消息到 Flutter 引擎,開始新的一次UI渲染。當 Flutter 引擎喚醒 SchedulerBinding 的時候,所有的變更都將生效,像魔法一樣。onDrawFrame()在前面,我們提到了 SchedulerBinding 兩個主要的功能,其中一個就是處理由 Flutter 引擎發出的與幀視圖重建相關的請求。下面,我們來看看有關它的詳細細節。當 SchedulerBinding 收到 Flutter 引擎發出的 onDrawFrame 時,執行的流程圖如下所示:
流程圖
WidgetsBinding被執行的時候, Flutter引擎首先考慮的是元素的變化。因為由建造者自己管理元素樹,所以綁定控件的時候,會調用建造者的 buildScope 方法。這個方法中,會將要更改的元素存起來,稍后觸發他們進行重建。
rebuild()
主要的原則如下:
大部分時候,觸發元素的重建,會調用控件的 build()
方法(Widget build(BuildContext context) {….}),這個方法會返回一個新的控件。
如果元素沒有子節點,這個元素就被創建完成,反之,會先創建子節點。
將新控件與元素引用的子控件進行比較:
如果可以被替換, 則更新,并保留子控件;
如果不可以被替換,子控件會被移除,并創建一個新的。
創建一個新的控件會創建一個與之對應的新的元素。并且會把元素插入到元素樹中去。下圖展示了這個過程:
當控件被創建的時候,需要創建一個元素與之進行關聯。
控件與元素的對應關系如下:
舉個例子:
StatefulElement 在初始化的時候會執行 widget.createState()
方法,創建并關聯對應的狀態。RenderObjectElement 在元素被加載的時候,會創建一個 RenderObject, 并且會將這個對象加入到渲染樹。
一旦完成了臟元素有關的所有動作,元素樹就變成了一個穩定結構,是時候考慮渲染到屏幕上了。
渲染綁定用來處理渲染樹,控件綁定會調用渲染綁定的 drawFrame 方法。
下圖展示了 drawFrame()
的整個調用流程:
這個過程中,主要執行以下的幾個事件:
在流程的最后,設備屏幕顯示的圖像將會被更新。
手勢處理
手指在屏幕上點擊或移動,觸發手勢,這些會被 GestureBinding 處理響應。
當 Flutter 引擎通過 window.onPointerDataPacket
方法發送出手勢相關的事件, GestureBinding 會攔截并處理:
動畫
最后一部分,我們將聚焦到動畫和 Ticker。
當你初始化一個動畫的時候,你通常會創建一個 AnimationController 或者類似的控件或者組件。
在Flutter中,與動畫相關的所有內容均為Ticker。
Tikcer 只做一件事情,它在 SchedulerBinding 上注冊一個回調,當Flutter引擎在下一次可用的時候,會將它喚醒。
當 Flutter 引擎可用,它會觸發 SchedulerBinding 的 "onBeginFrame" 通知。SchedulerBinding 會遍歷并執行所有的 Ticker 的回調。
Ticker 會被對此事件感興趣的攔截攔截,并對其進行處理。當動畫執行結束, Ticker會被標識為“不可用”。因此,Ticker 會告知 SchedulerBinding 去執行另一個回調。
總流程圖
現在,我們已經知道了 Flutter 內部的工作原理,來看看整體的流程圖:
總結構圖
BuildContext最后,如果你還記得不同元素類型的圖,你可能已經注意到了元素的簽名:dart abstract class Element extends DiagnosticableTree implements BuildContext { ...}
什么是 BuildContext ?
BuildContext是一個接口,它定義一系列元素要實現的方法。
特別的是, BuildCotext 中的 build() 方法在 StatelessWidget 和 StatefulWidget 中被使用,還有狀態對象在 StatefulWidget 中被使用。
除了一下的兩種情況下,其他時候 BuildContext 是沒有任何用處的:
- 控件被重建的時候;
- 在StatefulWidget鏈接到你引用的上下文變量的狀態。
這就意味著,大部分時候,我們并不需要知道它。
BuildContext 可以用來干什么?
由于BuildContext既與控件相關的元素相對應,也與空間所在樹中的位置相對應,因此該BuildContext對以下情況非常有用:
小例子
我們知道 BulidContext 也是一個元素,我給你展示一種有關 BuildContext 的使用方法。下面的代碼可以使 StatelessWidget 更新,但是并不使用 setState 方法,而是使用 BuildContext:
1 void main(){
2 runApp(MaterialApp(home: TestPage(),));
3 }
4
5 class TestPage extends StatelessWidget {
6 // final because a Widget is immutable (remember?)
7 final bag = {"first": true};
8
9 @override
10 Widget build(BuildContext context){
11 return Scaffold(
12 appBar: AppBar(title: Text('Stateless ??')),
13 body: Container(
14 child: Center(
15 child: GestureDetector(
16 child: Container(
17 width: 50.0,
18 height: 50.0,
19 color: bag["first"] ? Colors.red : Colors.blue,
20 ),
21 onTap: (){
22 bag["first"] = !bag["first"];
23 //
24 // This is the trick
25 //
26 (context as Element).markNeedsBuild();
27 }
28 ),
29 ),
30 ),
31 );
32 }
33 }
與執行 setState 方法相同,其核心都是執行 _element.markNeedsBuild()
方法。
我認為了解Flutter的架構是很有趣的,所有東西都被設計為高效,可擴展且對將來的擴展開放。而且,諸如Widget,Element,BuildContext,RenderObject之類的關鍵概念并不總是顯而易見。
我希望本文對你有用。
最多閱讀