在flutter
中,一切皆Widget
。無論是顯示介面的UI元素,如Text
、Image
、Icon
等;還是功能性元件,如手勢檢測的GestureDetector
元件、應用主題資料傳遞的Theme
元件、移除系統元件自帶Padding的MediaQuery
元件等。可以說,flutter
介面就是由一個個粒度非常細的Widget
組合起來的。
由於Widget
是不可變的,所以當檢視更新時,flutter
會建立新的Widget
來替換舊的Widget
並將舊的Widget
銷燬。但這樣就會涉及到大量Widget
物件的銷燬和重建,從而對垃圾回收造成壓力。也因此,flutter
將Widget
設計的十分輕量,並將檢視的配置資訊與渲染抽象出來,分別交給Element
與RenderObject
。從而使得Widget
只起一個組織者作用,可以將Element
與RenderObject
組合起來,構成一個檢視。
1、Widget介紹
前面說過Widget
是一種非常輕量且不可變的資料結構,只起一個組織者作用。那麼它是如何輕量的尼?下面我們就從原始碼來一窺究竟。
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
/// 建立Widget對應的Element物件,Element物件儲存了Widget的配置資訊
@protected
Element createElement();
/// 判斷是否可以更新Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
複製程式碼
Widget
是一個抽象類,它只有兩個方法:
- createElement:該方法是一個抽象方法,需要在子類實現。顧名思義,該方法主要是建立
Widget
對應的Element
物件。 - canUpdate:該方法主要是判斷
Widget
是否可更新。根據Widget
的runtimeType
與key
這兩個欄位來判斷。
由於Widget
可以將Element
與RenderObject
組合成一個檢視,但從上面原始碼我們可以發現,Widget
並沒有建立RenderObject
物件的方法,那麼它是如何建立RenderObject
物件的尼?其實是通過RenderObjectWidget
的createRenderObject
方法來建立的,此Widget
是一個非常重要的類,如果不直接或間接繼承該類,Widget
就無法顯示在介面上。下面我們對RenderObjectWidget
原始碼一窺究竟。
abstract class RenderObjectWidget extends Widget {
...
const RenderObjectWidget({ Key key }) : super(key: key);
/// RenderObjectWidget對應著RenderObjectElement及其子類
@override
RenderObjectElement createElement();
/// 建立一個RenderObject物件
@protected
RenderObject createRenderObject(BuildContext context);
/// 更新renderObject
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
/// 將renderObject從render樹中移除
@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
複製程式碼
RenderObjectWidget
是一個繼承自Widget
的子類,但它比Widget
多幾個方法。
- createRenderObject:建立
RenderObject
物件,在該物件中會將檢視資料繪製到不同的圖層上。筆者認為它對應著Android中ViewManager的addView方法。 - updateRenderObject:更新
Widget
所持有的RenderObject
物件。筆者認為它對應著Android中ViewManager的updateViewLayout方法。 - didUnmountRenderObject:將
RenderObject
物件從Render樹中移除,也就是銷燬RenderObject
物件。筆者認為它對應著Android中ViewManager的removeView方法。
由於RenderObject
主要是將檢視繪製成不同的圖層,然後再顯示在螢幕上。所以只有當我們的元件直接或間接繼承自RenderObjectWidget
時,才會通過RenderObject
來進行繪製、渲染,從而顯示在螢幕上,如RichText
、Row
、Center
等。否則只是一個用來組裝元件的容器,如Text
、ListView
等。
2、Element介紹
Element
是可變的,這裡的可變是指Element
擁有自己的生命週期,可以根據生命週期來重用或銷燬Element
物件,減少物件的頻繁建立及銷燬。它承載了檢視構建的上下文資料,也是Element
在連線Widget
與RenderObject
的橋樑,Element
與Widget
是一對多的關係。由於Element
是可變的,所以通過Element
將Widget
樹的變化(類似React虛擬DOM diff)做了抽象,可以將真正需要修改的部分同步到真實的RenderObject
樹中,最大程度降低對真實渲染檢視的修改,提高渲染效率,而不是銷燬整個渲染檢視樹重建。下面我們來對Element
的原始碼一窺究竟。
/// Element物件中儲存的是widget的配置資訊
abstract class Element extends DiagnosticableTree implements BuildContext {
...
/// 是一個非常重要的方法,主要是更新子Element的配置資訊。
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {...}
/// 將當前Element新增到Element樹中指定的位置,該位置由父級指定
/// 該方法會改變當前Element的狀態,由初始化(initial)狀態改為活動(active)狀態。
@mustCallSuper
void mount(Element parent, dynamic newSlot) {...}
/// 更新當前Element對應的Widget
/// 該方法僅在Element的活動(active)狀態期間呼叫
@mustCallSuper
void update(covariant Widget newWidget) {...}
/// 改變當前Element在父級中的位置
/// 在MultiChildRenderObjectElement或其他具有多個子元素的[RenderObjectElement]子類中呼叫。如:Flex及其子類(Column、Row)、Stack等
@protected
void updateSlotForChild(Element child, dynamic newSlot) {...}
void _updateSlot(dynamic newSlot) {...}
void _updateDepth(int parentDepth) {...}
/// 從render樹中移除當前Element所對應的RenderObject物件
void detachRenderObject() {...}
/// 將RenderObject物件新增到render樹中指定的位置上
void attachRenderObject(dynamic newSlot) {...}
...
/// 初始化Widget,建立Widget對應的Element物件。該方法會呼叫Widget的createElement方法
/// 該方法通常由updateChild方法呼叫,但也可以由需要對建立Element進行更細粒度控制的子類直接呼叫
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {...}
...
/// 將指定Element的狀態由活動狀態改為非活動狀態,並從Render樹中移除該Element持有的RenderObject物件
@protected
void deactivateChild(Element child) {...}
/// 從Element的子列表中移除指定的子Element,以準備在Element樹的其他地方重用子Element
@protected
void forgetChild(Element child);
void _activateWithParent(Element parent, dynamic newSlot) {...}
static void _activateRecursively(Element element) {...}
/// 將Element由初始化狀態改為活動狀態的具體實現,在mount方法中會呼叫該方法
@mustCallSuper
void activate() {...}
/// 將Element的狀態由活動轉變為非活動狀態(等待)。處於非活動狀態時,該Element不會被Widget使用,亦不會出現在螢幕上。在當前幀結束之前,該Element會一直存在,如果當前幀結束後,該Element尚未被使用,則該Element狀態將改變為銷燬狀態,從而銷燬該Element。
@mustCallSuper
void deactivate() {...}
...
/// 將Element的狀態由非活動(等待)狀態改為銷燬狀態,銷燬當前Element
@mustCallSuper
void unmount() {...}
@override
RenderObject findRenderObject() => renderObject;
//計算Widget的size,size是從RenderObject中獲取的的
@override
Size get size {...}
...
/// 當Element的依賴發生變化時會呼叫該方法
@mustCallSuper /// 這個是必須呼叫父類方法的註解吧???
void didChangeDependencies() {...}
...
/// 將Element元素標記為dirty並新增到全域性列表中,以便下次在下一幀中重新構建
void markNeedsBuild() {...}
/// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been
/// called to mark this element dirty, by [mount] when the element is first
/// built, and by [update] when the widget has changed.
void rebuild() {...}
/// 在進行一定的檢查後呼叫rebuild()方法
@protected
void performRebuild();
}
複製程式碼
Element
原始碼裡東西還是蠻多的,但其實只有以下一些核心的方法。
- updateChild:更新子
Element
,它主要有以下幾種情況。
newWidget == null | newWidget != null | |
---|---|---|
child == null | 返回null | 返回一個新的Element物件 |
child != null | 移除child並返回null | 如果可能就更新child,返回child或者一個新的Element物件。 |
- mount:將當前
Element
新增到Element
樹中指定的位置,該位置由父級指定。該方法會改變當前Element
的狀態,由初始化(initial)狀態改為活動(active)狀態。 - update:更新當前
Element
對應的Widget
。該方法僅在Element
的活動(active)狀態期間呼叫。 - updateSlotForChild:改變當前Element在父級中的位置。在
MultiChildRenderObjectElement
或其他具有多個子元素的RenderObjectElement
子類中呼叫。如:Flex
及其子類(Column
、Row
)、Stack
等。 - detachRenderObject:從render樹中移除當前
Element
所對應的RenderObject
物件。 - attachRenderObject:將
RenderObject
物件新增到render樹中指定的位置上。 - inflateWidget:初始化Widget,建立Widget對應的
Element
物件。該方法會呼叫Widget
的createElement
方法來建立Element
物件。該方法通常由updateChild方法呼叫,但也可以由需要對建立Element進行更細粒度控制的子類直接呼叫。 - deactivateChild:將子
Element
的狀態由活動狀態改為非活動狀態,並從render樹中移除該Element
持有的RenderObject
物件 - activate:將
Element
由初始化狀態改為活動狀態的具體實現,在mount
方法中會呼叫該方法 - deactivate:將
Element
的狀態由活動轉變為非活動狀態(等待)。處於非活動狀態時,該Element
不會被Widget
使用,亦不會出現在螢幕上。在當前幀結束之前,該Element
會一直存在,如果當前幀結束後,該Element
尚未被使用,則該Element
狀態將改變為銷燬狀態,從而銷燬該Element
。 - unmount:將
Element
的狀態由非活動(等待)狀態改為銷燬狀態,銷燬當前Element
。
從上面程式碼中,我們可以發現Element
在每一幀內都會在以下幾種狀態之間轉換。
- initial:初始化狀態,通過
createElement
方法建立了一個Elmenet
物件。 - active:活動狀態,通過
mount
方法將Elmenet
物件新增到了Elmenet
樹中。 - inactive:等待狀態,通過
deactivate
方法將Elmenet
物件從Elmenet
樹中移除。 - defunct:銷燬狀態,通過
unmount
方法將Elmenet
物件銷燬。
Element
的生命週期流程如下:
3、RenderObject介紹
RenderObject
與Widget
、Element
相比,乾的活是最苦逼的。因為要進行進行檢視的具體渲染,將檢視資料繪製成不同層級。如果沒有它,檢視就無法顯示在螢幕上。下面就從原始碼裡一窺究竟。
/// 顧名思義,主要是來繪製介面
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
/// 佈局開始
...
@override
void attach(PipelineOwner owner) {...}
/// 將當前RenderObject物件的佈局資訊標記為dirty
void markNeedsLayout() {...}
/// 將當前RenderObject物件的父物件的佈局資訊標記為dirty
@protected
void markParentNeedsLayout() {...}
/// 該方法裡僅呼叫了markNeedsLayout與markParentNeedsLayout方法
void markNeedsLayoutForSizedByParentChange() {...}
void _cleanRelayoutBoundary() {...}
void scheduleInitialLayout() {...}
void _layoutWithoutResize() {...}
/// 完成當前渲染物件的佈局,也是介面UI真正開始佈局的地方。對應著Android中View的layout方法
void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
...
/// 僅使用約束更新渲染物件大小。
/// 僅當[sizesByParent]為true時才呼叫此函式。
@protected
void performResize();
/// 為當前Render物件計算佈局大小,不能直接呼叫,由layout方法呼叫
@protected
void performLayout();
@protected
void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) {...}
/// 旋轉當前Render物件(尚未實現)
void rotate({
int oldAngle, // 0..3
int newAngle, // 0..3
Duration time
}) { }
/// 佈局結束
// 繪製開始
...
/// 將當前Render物件的合成狀態設為dirty
void markNeedsCompositingBitsUpdate() {...}
...
bool _needsPaint = true;
/// 將當前Render物件標記為需要重新繪製
void markNeedsPaint() {...}
void _skippedPaintingOnLayer() {...}
void scheduleInitialPaint(ContainerLayer rootLayer) {...}
/// 圖層替換。
void replaceRootLayer(OffsetLayer rootLayer) {...}
void _paintWithContext(PaintingContext context, Offset offset) {...}
...
/// 在該方法中進行真正的繪製,一般由子類重寫,
void paint(PaintingContext context, Offset offset) { }
...
/// 繪製結束
...
複製程式碼
別看RenderObject
原始碼那麼多。但核心方法其實只有兩個。
- layout:是一個抽象方法,在其子類中具體實現。它主要是實現介面的佈局,通過該方法就會給
Widget
指定其在螢幕上的位置。筆者認為它對應著Android中View的layout
方法。 - paint:是一個抽象方法,在其子類中具體實現。它主要是在介面上進行具體的繪製,介面上多姿多彩的介面就是通過該方法繪製的。筆者認為它對應著Android中View的
draw
方法。
通過RenderObject
就可以將一個個Widget
繪製成對應的圖層,由於圖層往往會非常多,所以直接向GPU傳遞這些圖層資料會非常低效。因此需要在Engine
中將這些圖層進行合併及光柵化,最後在將這些處理後的資料傳遞給GPU。
關於繪製原理的更多知識可以去YouTube看谷歌推出的講解視訊:Flutter’s renderding pipeline
4、演示案例
接下來用一個示例來說明Widget
、Element
及RenderObject
三者之間的關係。
_myWidget() {
return Center(
child: Column(
children: <Widget>[
Text("1111"),
Row(
children: <Widget>[Text("222"), Text("3333")],
),
Icon(Icons.ac_unit)
],
),
);
}
複製程式碼
在上面例子中,有一個Center
來讓Widget
居中展示,一個Column
來讓Widget
按照垂直方向排列,一個Row
來讓Widget
按照水平方向排列,多個Text
及Icon
來展示。
flutter
在對上面的Widget
遍歷完成以後,就會建立對應的Widget
樹、Element
樹及RenderObject
樹。如下:
注意:由於
Text
元件不持有RenderObject
物件,所以render樹中的Text
只是一個泛指。
可以發現,在上面的三個樹中,Widget
、Element
及RenderObject
是一一對應的,在Element
物件中會同時持有Widget
物件及RenderObject
物件。
5、結束語
當然,Widget
東西非常多,不可能僅憑一篇文章就能夠描述清楚的,本文也只是從整體結構上來對Widget
進行一次描述,方便大家深入的去了解Widget
。
最後來一個思考題,Flutter
的Widget
粒度為什麼那麼細?一位阿里大佬給了我一種思路。
由於Flutter借鑑了React Native的差分演算法來更新介面。那麼粒度越細,更新介面的效果就越好。
那麼大家以為尼???
【參考資料】