提出問題
用Flutter寫界面寫了一段時間了,感覺很爽,尤其是熱加載功能,節(jié)省了大把時間,聲明式的編程方式也是以后的趨勢。現(xiàn)在基本熟練以后一些簡單的效果能很快寫出來,即使沒見過的也可以在網(wǎng)上搜一下找到答案,但是感覺沒有深入底層了解,有些問題還是一知半解,這些問題比如以下幾個:
- createState 方法在什么時候調(diào)用?state 里面為啥可以直接獲取到 widget 對象?
- build 方法是在什么時候調(diào)用的?
- BuildContext 是什么?
- Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?
- 創(chuàng)建 Widget 里面的 Key 到底是什么作用?
后面抽時間看了一些關(guān)于 Flutter 渲染的文章,重點(diǎn)了解了 Widget、Element 和 RenderObject 方面的內(nèi)容,終于有了一些了解,對上面幾個問題也有了清晰的答案,因此通過這篇文章記錄一下。
三棵樹
首先先了解三棵樹,這是我們的核心,需要首先建立一個概念。
Widget 樹
我們平時用 Widget 使用聲明式的形式寫出來的界面,可以理解為 Widget 樹,這是要介紹的第一棵樹。
RenderObject 樹
Flutter 引擎需要把我們寫的 Widget 樹的信息都渲染到界面上,這樣人眼才能看到,跟渲染有關(guān)的當(dāng)然有一顆渲染樹 RenderObject tree,這是第二顆樹,渲染樹節(jié)點(diǎn)叫做 RenderObject,這個節(jié)點(diǎn)里面處理布局、繪制相關(guān)的事情。這兩個樹的節(jié)點(diǎn)并不是一一對應(yīng)的關(guān)系,有些 Widget是要顯示的,有些 Widget ,比如那些繼承自 StatelessWidget & StatefulWidget 的 Widget 只是將其他 Widget 做一個組合,這些 Widget 本身并不需要顯示,因此在 RenderObject 樹上并沒有相對應(yīng)的節(jié)點(diǎn)。
Element 樹
Widget 樹是非常不穩(wěn)定的,動不動就執(zhí)行 build方法,一旦調(diào)用 build 方法意味著這個 Widget 依賴的所有其他 Widget 都會重新創(chuàng)建,如果 Flutter 直接解析 Widget樹,將其轉(zhuǎn)化為 RenderObject 樹來直接進(jìn)行渲染,那么將會是一個非常消耗性能的過程,那對應(yīng)的肯定有一個東西來消化這些變化中的不便,來做cache。因此,這里就有另外一棵樹 Element 樹。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。
這三棵樹如下圖所示,是我們討論的核心內(nèi)容。
從上圖可以看出,widget 樹和 Element 樹節(jié)點(diǎn)是一一對應(yīng)關(guān)系,每一個 Widget 都會有其對應(yīng)的 Element,但是 RenderObject 樹則不然,只有需要渲染的 Widget 才會有對應(yīng)的節(jié)點(diǎn)。Element 樹相當(dāng)于一個中間層,大管家,它對 Widget 和 RenderObject 都有引用。當(dāng) Widget 不斷變化的時候,將新 Widget 拿到 Element 來進(jìn)行對比,看一下和之前保留的 Widget 類型和 Key 是否相同,如果都一樣,那完全沒有必要重新創(chuàng)建 Element 和 RenderObject,只需要更新里面的一些屬性即可,這樣可以以最小的開銷更新 RenderObject,引擎在解析 RenderObject 的時候,發(fā)現(xiàn)只有屬性修改了,那么也可以以最小的開銷來做渲染。
以上只是引出了非常重要的三棵樹和他們之間的關(guān)系,簡而言之,Widget 樹就是配置信息的樹,我們平時寫代碼寫的就是這棵樹,RenderObject 樹是渲染樹,負(fù)責(zé)計算布局,繪制,F(xiàn)lutter 引擎就是根據(jù)這棵樹來進(jìn)行渲染的,Element 樹作為中間者,管理著將 Widget 生成 RenderObject和一些更新操作。
前面只是從概念角度粗略來介紹,下面我們從源碼層面來看一看。
從源碼來了解 Widget、Element 和 RenderObject
Widget
下面對 Widget 的概述截圖來自官網(wǎng)
翻譯一下就是,Widget 描述 Element 的配置信息,是 Flutter 框架里的核心類層次結(jié)構(gòu),一個 Widget 是用戶界面某一部分的不可變描述。Widgets 可以轉(zhuǎn)為 Elements,Elements 管理著底層的渲染樹。
有這么多 Widget,我們來簡單分各類吧,前面已經(jīng)提到 Widget 有可渲染和不可渲染的分別了??射秩纠锩娣譃槎嗪⒆雍蛦魏⒆?,也就是屬性為 child 或 children,在不可渲染的 Widgets 里面又分為有狀態(tài)和無狀態(tài),也就是 StatefullWidget 和 StatelessWidget。我們選擇四個典型的Widgets來看看吧,如 Padding、RichText、Container、TextField。通過查閱源碼,我們看到這幾個類的繼承關(guān)系如下圖所示。
來到Widget 類里面可以看到有以下方法
@protected
Element createElement();
Widget 是個抽象類,所有的 Widgets 都是它的子類,其抽象方法 createElement 需要子類實現(xiàn),這里體現(xiàn)了之前我們說的 Widget 和 Element 的一一對應(yīng)關(guān)系。來到 StatelessWidget、StatefulWidget、MultiChildRenderObjectWidget、SingleChildRenderObjectWidget 里面我們可以找到 createElement 的實現(xiàn)。
SingleChildRenderObjectWidget
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
MultiChildRenderObjectWidget
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
StatefulWidget
@override
StatefulElement createElement() => StatefulElement(this);
StatelessWidget
@override
StatelessElement createElement() => StatelessElement(this);
可以發(fā)現(xiàn)規(guī)律,創(chuàng)建 Element 都會傳入 this,也就是當(dāng)前 Widget,然后返回對應(yīng)的 Element,這些 Element 都是繼承自 Element,Element 會有引用指向當(dāng)前 Widget。
我們繼續(xù)來到 RichText 和 Padding 類定義里面,他們都是繼承自 RenderObjectWidget,可以看到他們都有 createRenderObject 方法,如下
Padding
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.of(context),
);
}
RichText
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
locale: locale ?? Localizations.localeOf(context, nullOk: true),
);
}
RenderPadding 和 RenderParagraph 最終都是繼承自 RenderObject。通過以上源碼分析,我們可以看出來 Widget 里面有生成 Element 和 RenderObject 的方法,所以我們平時只需要埋頭寫好 Widget 就行,F(xiàn)lutter 框架會幫我們生成對應(yīng)的 Element 和 RenderObject。但是在什么時候調(diào)用 createElement 和 createRenderObject呢, 后面繼續(xù)分析。
Element
以下對 Element 描述來自官網(wǎng)
直接翻譯過來就是,Element 是 樹中特定位置 Widget 的一個實例化對象。這句話有兩層意思:1. 表示 Widget 是一個配置,Element 才是最終的對象;2. Element 是通過遍歷 Widget 樹時,調(diào)用 Widget 的方法創(chuàng)建的。Element 承載了視圖構(gòu)建的上下文數(shù)據(jù),是連接結(jié)構(gòu)化的配置信息到完成最終渲染的橋梁。
上面從源碼里面介紹 Widget 都會生成對應(yīng)的 Element,這里我們也對 Element 簡單做一個分類,和 Widget 相對應(yīng),如下圖所示。
首先還是進(jìn)入 Element 類里面看看,這是個抽象類,可以看到一些關(guān)鍵的方法和屬性。
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: assert(widget != null),
_widget = widget;
上面介紹Widget 里面 createElement 方法的時候可以看到會傳入 this,這里從 Element 的構(gòu)造方法中可以看到,this 最后傳給了 Element 里面的 _widget。也就是說每個 Element 里面都會有一個 Widget 的引用。_widget 在 Element 里面定義如下
/// The configuration for this element.
@override
Widget get widget => _widget;
Widget _widget;
從源碼里面知道 Element 里面的 widget 是一個 get 方法,直接返回 _widget。從上面的注釋信息也再一次提到 Widget 和 Element 的關(guān)系,Widget 是 Element 的配置。
對于 Element 的構(gòu)造方法,StatelessfulElement 有一些特殊的地方,如下
class StatefulElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
... 省略斷言 ...
assert(_state._element == null);
_state._element = this;
... 省略斷言 ...
_state._widget = widget;
assert(_state._debugLifecycleState == _StateLifecycle.created);
}
/// The [State] instance associated with this location in the tree.
///
/// There is a one-to-one relationship between [State] objects and the
/// [StatefulElement] objects that hold them. The [State] objects are created
/// by [StatefulElement] in [mount].
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
}
StatefulElement 的構(gòu)造方法中還調(diào)用了對應(yīng) Widget 的 createState 方法,并賦值給 _state,這也解答了我們在文章開頭提出的問題(createState 方法在什么時候調(diào)用?)。StatefulElement 里面不僅有對 Widget 的引用,也有對 StatefulWidget 的 State 的引用。并且在構(gòu)造函數(shù)里面還將 widget 賦值給了 _state 里面的 _widget。所以我們在 State 里面可以直接使用 widget 就可以拿到 State 對應(yīng)的 Widget。原來是在 StatefulElement 構(gòu)造函數(shù)的時候賦值的。解釋了開頭提到的問題(state 里面為啥可以直接獲取到 widget 對象?)。
Element 還有一個關(guān)鍵的方法 mount,如下
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
... 省略斷言 ...
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
_updateInheritance();
... 省略斷言 ...
}
Flutter 框架會根據(jù) Widget 創(chuàng)建對應(yīng)的 Element,Element 生成以后會調(diào)用 Element 的 mount 方法,將生成的 Element 掛載到 Element 樹上。這里的 createElement 和 mount 都是 Flutter 框架自動調(diào)用的,不需要開發(fā)者手動調(diào)用。因此我們平時可能沒關(guān)注這些過程。Element 里面的 mount 方法需要子類實現(xiàn),我們來看看ComponentElement 里的 mount 方法。
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild();
assert(_child != null);
}
這里一步一步看源碼,發(fā)現(xiàn)執(zhí)行鏈路如下:
_firstBuild()【ComponentElement】 -> rebuild() 【Element】-> performRebuild()【ComponentElement】 -> build()【StatelessElement】
看一看最后 StatelessElement build() 的源碼
@override
StatelessWidget get widget => super.widget;
@override
Widget build() => widget.build(this);
StatefulElement 的 build() 的源碼如下
@override
Widget build() => state.build(this);
可以看出ComponentElement 的 mount 最后執(zhí)行的是 build 方法。不過 StatelessElement 和 StatefulElement 是有區(qū)別的,StatelessElement 執(zhí)行的是 Widget 里的 build 方法,而 StatefulElement 里面執(zhí)行的是 state 的 build 方法。因此,這里也解決了文章開始提到的一個問題(build 方法是在什么時候調(diào)用的?)。也知道了 StatefulWidget 和 它的 State 是如何聯(lián)系起來的。
另外,我們看到上面執(zhí)行執(zhí)行build 方法傳遞的參數(shù) this,也就是當(dāng)前 Element,而我們在寫代碼的時候 build 方法是這樣的
@override
Widget build(BuildContext context) {
}
因此我們知道了,這個 BuildContext 其實就是這個 Widget 所對應(yīng)的 Element。看看 Element 的定義就更清楚了。這也解釋了開始提到的問題(BuildContext 是什么?)。
abstract class Element extends DiagnosticableTree implements BuildContext {
}
再來看看 RenderObjectElement 里的 mount 方法
@override
void mount(Element parent, dynamic newSlot) {
... 省略斷言 ...
_renderObject = widget.createRenderObject(this);
... 省略斷言 ...
attachRenderObject(newSlot);
_dirty = false;
}
對比一下 ComponentElement 和 RenderObjectElement 里面的 mount 方法,前面介紹過,ComponentElement 是非渲染 Widget 對應(yīng)的 Element,而 RenderObjectElement 是渲染 Widget 對應(yīng)的 Element,前者的mount 方法主要是負(fù)責(zé)執(zhí)行 build 方法,而后者的 mount 方法主要是調(diào)用 Widget 里面的 createRenderObject 方法生成 RenderObject,然后賦值給自己的 _renderObject。
因此可以總結(jié),ComponentElement 的 mount 方法主要作用是執(zhí)行 build,而 RenderObjectElement 的 mount 方法主要作用是生成 RenderObject。
Widget 類里面有一個很重要的靜態(tài)方法,本來可以放到上面講 Widget 的時候說,但是還是放到 Element 里面吧。就是這個
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Element 里面有一個 _widget 作為其配置信息,當(dāng)widget變化或重新生成以后,Element 要不要銷毀重建呢,還是直接將新生成的 Widget 替換舊的 Widget。答案就是通過這個方法判斷的,上面的注釋可以翻譯如下
判斷新 Widget 是否可以用來取代 Element 當(dāng)前的配置信息 _widget。
Element 使用特定的 widget 作為其配置信息,如果 runtimeType 和 key 和之前的 widget 相同,那么可以使用一個新的 widget 更新 Element 里面舊的 widget。
如果這兩個widget 都沒有賦值 key,那么只要 runtimeType 相同也可以更新,即使這兩個 widget 的孩子 widget 都完全不一樣。
因此可以看出,即使外面的 widget 樹經(jīng)常變換重建,我們的 Element 可以維持相對穩(wěn)定,不會重復(fù)創(chuàng)建,當(dāng)然也就不會重復(fù) mount, 生成 RenderObject,只需要以最小代價更新相關(guān)屬性即可,最大可能減小了性能消耗。Widget 本身只是一些配置信息,簡單的對象,它的變更重建不直接影響渲染,對性能影響很小。這就解決了上文提到的另外一個問題(Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?)。
RenderObject
從 RenderObject 的名字,我們就能很直觀地知道,RendreObject 是主要負(fù)責(zé)實現(xiàn)視圖渲染的對象。從上文中我們知道了一下幾點(diǎn)
- RenderObject 和 widget 并不是一一對應(yīng)的,只有繼承自 RenderObjectWidget 的 widget 才有對應(yīng)的 RenderObject;
- 生成 RenderObject 的方法 createRenderObject 是在 Widget 里面定義的;
- 在 RenderObjectElement 執(zhí)行 mount 方法的時候調(diào)用的 widget 里面的 createRenderObject 方法的;
- RenderObjectElement 里面既有對 Widget 的引用也有對 RenderObject 的引用,它作為中間者,管理著雙方。
RenderObject 在 Flutter 的展示分為四個階段,即布局、繪制、合成和渲染。其中,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機(jī)制遍歷渲染對象樹,確定樹中各個對象的位置和尺寸,并把它們繪制在不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 搞定。
總結(jié)
上面通過源碼講解了一下 Widget、Element、RenderObject 的聯(lián)系。下面簡單來個總結(jié)。
我們寫好 Widget 樹后,F(xiàn)lutter 會在遍歷 Widget 樹時調(diào)用 Widget 里面的 createElement 方法去生成對應(yīng)節(jié)點(diǎn)的 Element 對象,同時 Element 里面也有了對 Widget 的引用。特別的是當(dāng) StatefulElement 創(chuàng)建的時候也執(zhí)行 StatefulWidget 里面的 createState 方法創(chuàng)建 state,并且賦值給 Element 里的 _state 屬性,當(dāng)前 widget 也同時賦值給了 state 里的_widget。Element 創(chuàng)建好以后 Flutter 框架會執(zhí)行 mount 方法,對于非渲染的 ComponentElement 來說 mount 主要執(zhí)行 widget 里的 build 方法,而對于渲染的 RenderObjectElement 來說 mount 里面會調(diào)用 widget 里面的 createRenderObject 方法 生成 RenderObject,并賦值給 RenderObjectElement 里的相應(yīng)屬性。StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是 常見的 BuildContext。
如果 Widget 的配置數(shù)據(jù)發(fā)生了改變,那么持有該 Widget 的 Element 節(jié)點(diǎn)也會被標(biāo)記為 dirty。在下一個周期的繪制時,F(xiàn)lutter 就會觸發(fā)該 Element 樹的更新,通過 canUpdate 方法來判斷是否可以使用新的 Widget 來更新 Element 里面的配置,還是重新生成 Element。并使用最新的 Widget 數(shù)據(jù)更新自身以及關(guān)聯(lián)的 RenderObject對象。布局和繪制完成后,接下來的事情交給 Skia 了。在 VSync 信號同步時直接從渲染樹合成 Bitmap,然后提交給 GPU。
回答開頭提出的問題
- createState 方法在什么時候調(diào)用?state 里面為啥可以直接獲取到 widget 對象?
答:Flutter 會在遍歷 Widget 樹時調(diào)用 Widget 里面的 createElement 方法去生成對應(yīng)節(jié)點(diǎn)的 Element 對象,同時執(zhí)行 StatefulWidget 里面的 createState 方法創(chuàng)建 state,并且賦值給 Element 里的 _state 屬性,當(dāng)前 widget 也同時賦值給了 state 里的_widget,state 里面有個 widget 的get 方法可以獲取到 _widget 對象。
- build 方法是在什么時候調(diào)用的?
答:Element 創(chuàng)建好以后 Flutter 框架會執(zhí)行 mount 方法,對于非渲染的 ComponentElement 來說 mount 主要執(zhí)行 widget 里的 build 方法,StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是常見的 BuildContext
- BuildContext 是什么?
答:StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是 常見的 BuildContext。簡而言之 BuidContext 就是 Element。
- Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?
答:不會影響性能,widget 只是簡單的配置信息,并不直接涉及布局渲染相關(guān)。Element 層通過判斷新舊 widget 的runtimeType 和 key 是否相同決定是否可以直接更新之前的配置信息,也就是替換之前的 widget,而不必每次都重新創(chuàng)建新的 Element。
- 創(chuàng)建 Widget 里面的 Key 到底是什么作用?
答:Key 作為 Widget 的標(biāo)志,在widget 變更的時候通過判斷 Element 里面之前的 widget 的 runtimeType 和 key來決定是否能夠直接更新。需要了解更多 Key 的作用可以閱讀這邊文章Flutter渲染之通過demo了解Key的作用。通過幾個demo可以好好鞏固這篇文章的知識。