在Flutter中,一切都是由Widget組成,不管是按鈕、文本、圖像、列表、布局、手勢、動畫處理等都可以作為Widget,開發(fā)者通過組合、嵌套Widget構(gòu)建UI界面。
這篇文章將探索 Flutter Widget 背后的設(shè)計思想,深入分析源碼以弄清它的實現(xiàn)原理,從而讓我們更好地使用 Widget 開發(fā) UI 界面。
設(shè)計思想
Flutter 從 React 中吸取靈感,通過現(xiàn)代化框架創(chuàng)建出精美的組件。它的核心思想是用 widget 來構(gòu)建你的 UI 界面。Widget 描述了在當(dāng)前的配置和狀態(tài)下視圖所應(yīng)該呈現(xiàn)的樣子。當(dāng) widget 的狀態(tài)改變時,它會重新構(gòu)建其要展示的 UI,框架則會對比前后變化的不同,以確定底層渲染樹從一個狀態(tài)轉(zhuǎn)換到下一個狀態(tài)所需的最小更改。
這是官方對 Widget 的介紹,可以看出,F(xiàn)lutter 設(shè)計靈感來自于 React,React 的核心聲明式和組件化編程,F(xiàn)lutter 都繼承了下來,Widget 同樣使用聲明式和組件化編程范式。
編程范式,是一種編程風(fēng)格,它提供了(同時決定了)程序員對程序執(zhí)行的看法。例如,在面向?qū)ο缶幊讨?,程序員認(rèn)為程序是一系列相互作用的對象,而在函數(shù)式編程中一個程序會被看作是一個無狀態(tài)的函數(shù)計算的序列。
聲明式編程
聲明式是一種編程范式,描述 UI 是什么樣的,而不是直接指導(dǎo) UI 怎么一步步地構(gòu)建。通常與聲明式編程相對的是命令式編程,命令式編程需要用算法來明確的指出每一步該怎么做。
對于聲明式編程,當(dāng)我們要渲染界面時,無需編寫操作視圖命令的代碼,而是修改數(shù)據(jù),由框架完成數(shù)據(jù)到視圖的轉(zhuǎn)換。數(shù)據(jù)是組件的UI數(shù)據(jù)模型,開發(fā)者根據(jù)需要設(shè)計出合理的數(shù)據(jù)模型,框架根據(jù)數(shù)據(jù)來渲染出UI界面。這種方式讓開發(fā)人員只需要管理和維護(hù)數(shù)據(jù)狀態(tài),大大減輕了開發(fā)人員的負(fù)擔(dān)。
iOS 開發(fā)中的 UITableView 的使用與聲明式編程較類似,我們作個比較來幫助理解聲明式編程。通常先準(zhǔn)備好數(shù)據(jù)源 dataSource,然后將 dataSource 中的 items 映射成一個個 cell,當(dāng)數(shù)據(jù)源 dataSource 改變時,UITableView 就會相應(yīng)的刷新。
我們再舉個例子,對比說明命令式編程和聲明式編程的不同

在命令式編程中,通常使用選擇器 findViewById 或類似函數(shù)獲取到 ViewB 的實例 b,并調(diào)用相關(guān)的方法使用其生效,如下
// 命令式風(fēng)格
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)
而在聲明式編程中,當(dāng)UI需要改變時,我們在 StatefulWidgets 組件上調(diào)用 setState()改變數(shù)據(jù),重建新的Widget樹。
// 聲明式網(wǎng)絡(luò)
return ViewB(
color: red,
child: ViewC(...),
)
當(dāng)按照上面的數(shù)據(jù)驅(qū)動視圖的方式構(gòu)建 UI,會出現(xiàn)一個問題,視圖中的任一狀態(tài)變化,都會重新渲染整個視圖,導(dǎo)致不必要的刷新,那 React/Flutter 如何避免這個問題的?
在 React 中用 Component 描述界面,在 Flutter 中用 Widget 描述界面,Component 和 Widget 都是視圖內(nèi)容的“配置信息”,并不是真正渲染在屏幕的元素,這些配置對象的創(chuàng)建、銷毀不會帶來太大的性能損耗。而真正負(fù)責(zé)渲染繪制的對象重建代價是很高的,不會輕易重建。
當(dāng)數(shù)據(jù)狀態(tài)有變動時,框架重新計算生成新的組件樹,并比較新、舊組件樹的差異,找出有變化的組件進(jìn)行重新渲染。這樣就只渲染了有變化的組件,沒變化的組件不刷新,避免了整體刷新帶來的無謂性能損耗。
組件化
在 React/Flutter 的世界中,一切都是組件,組件是對一個 UI 元素的配置或描述,描述了在屏幕上展示的內(nèi)容,可以說用戶界面就是組件,組件嵌套組合構(gòu)成了用戶界面。
組件,可以看成是一個狀態(tài)機,通過與用戶的交互,改變不同狀態(tài),當(dāng)組件處于某個狀態(tài)時輸出對應(yīng)的UI,組件狀態(tài)改變時,根據(jù)新的狀態(tài)重新渲染視圖,數(shù)據(jù)與視圖始終保持一致。
組件是一個比較獨立的個體,自身包含了邏輯/樣式/布局,甚至是依賴的靜態(tài)資源,相關(guān)代碼都封裝到一個單元中,盡可能地避免與其他代碼產(chǎn)生糾葛。這種設(shè)計符合高內(nèi)聚、低耦合,組件盡可能獨立完成自己的功能,不依賴外部的代碼。
一個個簡單的組件通過嵌套、組合的方式構(gòu)成大的組件,最終再構(gòu)建成復(fù)雜的界面,這樣搭建界面的方式易于理解、易于維護(hù)。
思考:React/Flutter 中的組件是 MVVM 嗎
實現(xiàn)原理
在 Flutter 中,Widget 的功能是對一個 UI 元素的配置或描述,并不是真正在設(shè)備上顯示的元素。真正表示在設(shè)備顯示元素的類是 Element,真正負(fù)責(zé)布局和繪制的類是 RenderObject。
我們從幾個問題開始,理清 Widget 的實現(xiàn)原理
1,Widget、Element 和 RenderObject 是什么,它們各自負(fù)責(zé)什么功能?
2,Widget、Element 和 RenderObject,三者有什么關(guān)系?
3,Widget、Element 和 RenderObject,它們是如何生成的、如何建立關(guān)聯(lián)?
4,頁面中 Widget 更新時,視圖如何重新渲染?
5,Element 樹如何更新?
1,Widget、Element 和 RenderObject 是什么,它們各自負(fù)責(zé)什么功能?
Widget 是對一個 UI 元素的配置或描述,存放渲染內(nèi)容、布局信息等。
對于 Widget,它是不可變的,一經(jīng)創(chuàng)建便不能修改。當(dāng)用戶界面發(fā)生變化時,F(xiàn)lutter不會修改舊的Widget樹,而是創(chuàng)建新的 Widget 樹。由于 Widget 很輕量,只是一個“藍(lán)圖”,并不涉及實際的視圖渲染,
頻繁的銷毀和重建也不會帶來性能問題。
Element 是通過 Widget 生成的,是 Widget 的實例化對象,它們之間有一一對應(yīng)關(guān)系。Element 同時持有 Widget 和 RenderObject,是連接配置信息到最終渲染的橋梁。
Element 被創(chuàng)建之后,將插入到 UI 樹中。如果之后 Widget 發(fā)生變化,則將其與舊的 Widget 進(jìn)行比較,并更新對應(yīng)的 Element。
由于 Widget 的不可變性,當(dāng) Widget 樹重建時,Element 樹將對比新舊 Widget 樹,找到有變化的節(jié)點,并同步到 RenderObject 樹,最終只渲染有變化的部分節(jié)點,提高渲染效率。
RenderObject 具體負(fù)責(zé)布局和繪制工作。
2,Widget、Element 和 RenderObject,三者有什么關(guān)系?
Flutter 的 UI 系統(tǒng)中有三棵樹:Widget 樹、Element 樹、渲染樹,它們的關(guān)系是:Element 樹根據(jù) Widget 樹生成,渲染樹又根據(jù) Element 生成。
當(dāng) Widget 樹發(fā)生改變時,將重新構(gòu)建對應(yīng)的 Element 樹,同時更新渲染樹。
3,Widget、Element和RenderObject,它們是如何生成的、如何建立關(guān)聯(lián)?
Flutter 程序的入口是 void runApp(Widget app) 方法,應(yīng)用啟動時調(diào)用。該方法傳入要展示的第一個 rootWidget,然后創(chuàng)建 rootElement,并把 rootWidget 關(guān)聯(lián)到 rootElement.widget 屬性上。rootElement 創(chuàng)建完成后,調(diào)用自身的 mount 方法創(chuàng)建 rootRenderObject 對象,并把rootRenderObject 關(guān)聯(lián)到 rootElement.renderObject 屬性上。
rootElement 創(chuàng)建完成后,調(diào)用 buildScope 方法,進(jìn)行 child widget 樹的創(chuàng)建。widget 與 element 一一對應(yīng),child widget 調(diào)用 createElement 方法,以自身為參數(shù)創(chuàng)建 child element,然后 child element 將自身掛載到 rootElement 上,形成一棵樹。
同時調(diào)用 widget.createRenderObject 創(chuàng)建 child renderObject,并掛載到 rootRenderObject 上。
其中 rootElement 和 renderView(RenderObject子類)是全局單例對象,只會創(chuàng)建一次。

4,頁面中Widget更新時,視圖如何重新渲染?
Widget 的子類 StatefullWidget 能夠創(chuàng)建對應(yīng)的 State 對象,通過調(diào)用 state.setState() 方法觸發(fā)視圖的刷新。
state.setState() 方法內(nèi)部調(diào)用了 markNeedsBuild,標(biāo)記該 StatefullWidget 對應(yīng)的 Element 需要刷新
_element.markNeedsBuild();
當(dāng)下一個周期的幀繪制 drawFrame 時,重新調(diào)用 performRebuild(),觸發(fā) Element 樹更新,并使用最新的 Widget 樹更新自身以及關(guān)聯(lián)的 RenderObject 對象,之后進(jìn)入行布局和繪制流程。
5,Element樹如何更新?
當(dāng)有 StatefullWidget 發(fā)生改變時,找到對應(yīng)的 element 節(jié)點,設(shè)置它的 dirty 為 true。當(dāng)下一次幀繪制 drawFrame 時,重新調(diào)用 performRebuild() 更新 UI
| newWidget == null | newWidget != null | |
|---|---|---|
| child == null | Returns null. | Returns new [Element]. |
| child != null | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
如上所示,新 widget 與舊的 child 內(nèi)的 widget 進(jìn)行比較,有4種情形,
新 widget 為空、舊 widget 也為空,返回 null
新widget為空、舊widget不為空,則移除舊child,返回null
新widget不為空、舊widget為空,創(chuàng)建新的Element并調(diào)用mount嵌入到樹上
新widget不為空、舊widget不為空,判斷是否可更新舊child,可以則更新child。不可以則移除舊child,創(chuàng)建新的Element并返回
源碼分析
我們從一個Hellow world的demo開始分析Widget源碼。
程序的入口是 runApp 方法,它傳入要顯示的界面Widget。
void main() {
runApp(
Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
進(jìn)入 runApp 方法,其中 WidgetsFlutterBinding 是一個橋接類,它是連接底層 Flutter engine SDK 的橋梁,用來接收處理 Flutter engine 傳遞過來的消息,F(xiàn)lutter engine 負(fù)責(zé)布局、繪制、平臺消息、手勢等功能。
WidgetsFlutterBinding 繼承自 BindingBase,BindingBase mixin 了7個類:GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding。mixin 類似于多繼承,在 mixin 模式中,后面類的同名方法會覆蓋前面類的方法。這些類組合到一塊共同監(jiān)聽來的 Flutter engine 的各種消息。
WidgetsFlutterBinding 是一個單例類,ensureInitialized 方法負(fù)責(zé)初始化,返回單例對象
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
attachRootWidget 方法傳入 rootWidget,將 rootWidget 和 renderView(RenderObject子類)包裝到 RenderObjectToWidgetAdapter 中。renderView 是上面提到的RendererBinding 在初始化時創(chuàng)建,它是 RenderObject 的子類,負(fù)責(zé)實際的布局和繪制工作。
RenderObjectToWidgetAdapter 繼承自 Widget,重寫了 createElement 和 createRenderObject 方法,這兩個方法在后面構(gòu)建Element 和 RenderObject 會用到,createElement 返回的是RenderObjectToWidgetElement 類型對象,createRenderObject 返回的是 renderView。
renderViewElement 和 renderView 都是 WidgetsFlutterBinding 類的屬性,而 WidgetsFlutterBinding 是單例模式,自然 renderViewElement 和 renderView 全局唯一。
Element get renderViewElement => _renderViewElement;
Element _renderViewElement;
void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: ‘[root]’,
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
attachToRenderTree 方法創(chuàng)建并返回 RenderObjectToWidgetElement 對象,首次調(diào)用時創(chuàng)建新的 RenderObjectToWidgetElement 對象,再次調(diào)用復(fù)用已有的。
createElement 方法將 RenderObjectToWidgetAdapter 自身作為參數(shù),初始化 RenderObjectToWidgetElement 對象,這樣 RenderObjectToWidgetElement 就可以取到 rootWidget 和 renderView,從而讓三者關(guān)聯(lián)起來。
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
隨著 Element 的創(chuàng)建,RenderObject 也會被創(chuàng)建。在父類 RenderObjectElement的mount 方法中,調(diào)用 createRenderObject 得到RenderObject,這里的 widget 即是 RenderObjectToWidgetAdapter,_renderObject 是 RenderObjectToWidgetAdapter 持有的 renderView 對象
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); //[見小節(jié)2.5.5]
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot); //將newSlot依附到RenderObject上
_dirty = false;
}
到這里,Element 和 RenderObject 都被創(chuàng)建出來。回到 RenderObjectToWidgetElement 的 mount 方法,它調(diào)用了 _rebuild 方法,_rebuild 又調(diào)用了 updateChild 方法
void mount(Element parent, dynamic newSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
}
void _rebuild() {
try {
_child = updateChild(_child, widget.child, _rootChildSlot);
} catch (exception, stack) {
...
}
}
在 updateChild 方法中,對新舊節(jié)點的 widget 進(jìn)行對比,有4種情形,
新widget為空、舊widget也為空,返回null
新widget為空、舊widget不為空,則移除舊child,返回null
新widget不為空、舊widget為空,創(chuàng)建新的Element并調(diào)用mount嵌入到樹上
新widget不為空、舊widget不為空,判斷是否可更新舊child,可以則更新child。不可以則移除舊child,創(chuàng)建新的Element并返回
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
return inflateWidget(newWidget, newSlot);
}
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
至此,app 從啟動到首次渲染的過程就完成了,接下來我們看看,當(dāng)頁面 Widget 更新時,內(nèi)部做了什么,我們從 setState() 方法開始分析
abstract class State<T extends StatefulWidget> extends Diagnosticable {
StatefulElement _element;
void setState(VoidCallback fn) {
...
_element.markNeedsBuild();
}
}
將 element.dirty 設(shè)為 true,即標(biāo)記該 element 需要刷新,并將其放到臟元素數(shù)組中,等待下一個周期渲染時處理。
onBuildScheduled 是 WidgetsBinding 初始化時創(chuàng)建的,方法內(nèi)部又調(diào)用了 ui.window.scheduleFrame(),通知底層 engine 重新刷新幀
abstract class Element extends DiagnosticableTree implements BuildContext {
void markNeedsBuild() {
if (!_active)
return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
}
void scheduleBuildFor(Element element) {
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
Engine 通知頁面刷新,最終調(diào)到 drawFram 方法內(nèi)的 buildScope,buildScope 首先對臟元素數(shù)組排序,淺節(jié)點靠前,深節(jié)點靠后,避免子節(jié)點先重建,父節(jié)點重建后再次重建。
void drawFrame() {
try {
buildOwner.buildScope(renderViewElement);
......
buildOwner.finalizeTree();
} finally {
}
}
void buildScope(Element context, [VoidCallback callback]) {
try {
...
_dirtyElements.sort(Element._sort);//對臟元素排序
...
int dirtyCount = _dirtyElements.length;
int index = 0;
//遍歷臟元素,重建
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
...
}
}
Element 的 rebuild 方法最終會調(diào)用 performRebuild(), performRebuild 又調(diào)用了 updateChild 方法,此方法在前面有介紹,在 updateChild 方法中,對新舊節(jié)點的 widget 進(jìn)行對比,有4種情形,只更新需要變更的節(jié)點。
參考資料
Flutter
(一):React的設(shè)計哲學(xué) - 簡單之美
帝國的紛爭-FlutterUI繪制解析
深入理解setState更新機制
Flutter 在銘師堂的實踐