Flutter 探索系列:Widget 原理(一)

在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)的刷新。

我們再舉個例子,對比說明命令式編程和聲明式編程的不同

image.png

在命令式編程中,通常使用選擇器 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)建一次。

image.png

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 在銘師堂的實踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容