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

在Flutter中,一切都是由Widget組成,不管是按鈕、文本、圖像、列表、布局、手勢、動畫處理等都可以作為Widget,開發者通過組合、嵌套Widget構建UI界面。

這篇文章將探索 Flutter Widget 背后的設計思想,深入分析源碼以弄清它的實現原理,從而讓我們更好地使用 Widget 開發 UI 界面。

設計思想

Flutter 從 React 中吸取靈感,通過現代化框架創建出精美的組件。它的核心思想是用 widget 來構建你的 UI 界面。Widget 描述了在當前的配置和狀態下視圖所應該呈現的樣子。當 widget 的狀態改變時,它會重新構建其要展示的 UI,框架則會對比前后變化的不同,以確定底層渲染樹從一個狀態轉換到下一個狀態所需的最小更改。

這是官方對 Widget 的介紹,可以看出,Flutter 設計靈感來自于 React,React 的核心聲明式和組件化編程,Flutter 都繼承了下來,Widget 同樣使用聲明式和組件化編程范式。

編程范式,是一種編程風格,它提供了(同時決定了)程序員對程序執行的看法。例如,在面向對象編程中,程序員認為程序是一系列相互作用的對象,而在函數式編程中一個程序會被看作是一個無狀態的函數計算的序列。

聲明式編程

聲明式是一種編程范式,描述 UI 是什么樣的,而不是直接指導 UI 怎么一步步地構建。通常與聲明式編程相對的是命令式編程,命令式編程需要用算法來明確的指出每一步該怎么做。

對于聲明式編程,當我們要渲染界面時,無需編寫操作視圖命令的代碼,而是修改數據,由框架完成數據到視圖的轉換。數據是組件的UI數據模型,開發者根據需要設計出合理的數據模型,框架根據數據來渲染出UI界面。這種方式讓開發人員只需要管理和維護數據狀態,大大減輕了開發人員的負擔。

iOS 開發中的 UITableView 的使用與聲明式編程較類似,我們作個比較來幫助理解聲明式編程。通常先準備好數據源 dataSource,然后將 dataSource 中的 items 映射成一個個 cell,當數據源 dataSource 改變時,UITableView 就會相應的刷新。

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

image.png

在命令式編程中,通常使用選擇器 findViewById 或類似函數獲取到 ViewB 的實例 b,并調用相關的方法使用其生效,如下

// 命令式風格
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)

而在聲明式編程中,當UI需要改變時,我們在 StatefulWidgets 組件上調用 setState()改變數據,重建新的Widget樹。

// 聲明式網絡
return ViewB(
  color: red,
  child: ViewC(...),
)

當按照上面的數據驅動視圖的方式構建 UI,會出現一個問題,視圖中的任一狀態變化,都會重新渲染整個視圖,導致不必要的刷新,那 React/Flutter 如何避免這個問題的?

在 React 中用 Component 描述界面,在 Flutter 中用 Widget 描述界面,Component 和 Widget 都是視圖內容的“配置信息”,并不是真正渲染在屏幕的元素,這些配置對象的創建、銷毀不會帶來太大的性能損耗。而真正負責渲染繪制的對象重建代價是很高的,不會輕易重建。

當數據狀態有變動時,框架重新計算生成新的組件樹,并比較新、舊組件樹的差異,找出有變化的組件進行重新渲染。這樣就只渲染了有變化的組件,沒變化的組件不刷新,避免了整體刷新帶來的無謂性能損耗。

組件化

在 React/Flutter 的世界中,一切都是組件,組件是對一個 UI 元素的配置或描述,描述了在屏幕上展示的內容,可以說用戶界面就是組件,組件嵌套組合構成了用戶界面。

組件,可以看成是一個狀態機,通過與用戶的交互,改變不同狀態,當組件處于某個狀態時輸出對應的UI,組件狀態改變時,根據新的狀態重新渲染視圖,數據與視圖始終保持一致。

組件是一個比較獨立的個體,自身包含了邏輯/樣式/布局,甚至是依賴的靜態資源,相關代碼都封裝到一個單元中,盡可能地避免與其他代碼產生糾葛。這種設計符合高內聚、低耦合,組件盡可能獨立完成自己的功能,不依賴外部的代碼。

一個個簡單的組件通過嵌套、組合的方式構成大的組件,最終再構建成復雜的界面,這樣搭建界面的方式易于理解、易于維護。

思考:React/Flutter 中的組件是 MVVM 嗎

實現原理

在 Flutter 中,Widget 的功能是對一個 UI 元素的配置或描述,并不是真正在設備上顯示的元素。真正表示在設備顯示元素的類是 Element,真正負責布局和繪制的類是 RenderObject。

我們從幾個問題開始,理清 Widget 的實現原理
1,Widget、Element 和 RenderObject 是什么,它們各自負責什么功能?
2,Widget、Element 和 RenderObject,三者有什么關系?
3,Widget、Element 和 RenderObject,它們是如何生成的、如何建立關聯?
4,頁面中 Widget 更新時,視圖如何重新渲染?
5,Element 樹如何更新?

1,Widget、Element 和 RenderObject 是什么,它們各自負責什么功能?

Widget 是對一個 UI 元素的配置或描述,存放渲染內容、布局信息等。

對于 Widget,它是不可變的,一經創建便不能修改。當用戶界面發生變化時,Flutter不會修改舊的Widget樹,而是創建新的 Widget 樹。由于 Widget 很輕量,只是一個“藍圖”,并不涉及實際的視圖渲染,
頻繁的銷毀和重建也不會帶來性能問題。

Element 是通過 Widget 生成的,是 Widget 的實例化對象,它們之間有一一對應關系。Element 同時持有 Widget 和 RenderObject,是連接配置信息到最終渲染的橋梁。

Element 被創建之后,將插入到 UI 樹中。如果之后 Widget 發生變化,則將其與舊的 Widget 進行比較,并更新對應的 Element。

由于 Widget 的不可變性,當 Widget 樹重建時,Element 樹將對比新舊 Widget 樹,找到有變化的節點,并同步到 RenderObject 樹,最終只渲染有變化的部分節點,提高渲染效率。

RenderObject 具體負責布局和繪制工作。

2,Widget、Element 和 RenderObject,三者有什么關系?

Flutter 的 UI 系統中有三棵樹:Widget 樹、Element 樹、渲染樹,它們的關系是:Element 樹根據 Widget 樹生成,渲染樹又根據 Element 生成。

當 Widget 樹發生改變時,將重新構建對應的 Element 樹,同時更新渲染樹。

3,Widget、Element和RenderObject,它們是如何生成的、如何建立關聯?

Flutter 程序的入口是 void runApp(Widget app) 方法,應用啟動時調用。該方法傳入要展示的第一個 rootWidget,然后創建 rootElement,并把 rootWidget 關聯到 rootElement.widget 屬性上。rootElement 創建完成后,調用自身的 mount 方法創建 rootRenderObject 對象,并把rootRenderObject 關聯到 rootElement.renderObject 屬性上。

rootElement 創建完成后,調用 buildScope 方法,進行 child widget 樹的創建。widget 與 element 一一對應,child widget 調用 createElement 方法,以自身為參數創建 child element,然后 child element 將自身掛載到 rootElement 上,形成一棵樹。

同時調用 widget.createRenderObject 創建 child renderObject,并掛載到 rootRenderObject 上。

其中 rootElement 和 renderView(RenderObject子類)是全局單例對象,只會創建一次。

image.png

4,頁面中Widget更新時,視圖如何重新渲染?

Widget 的子類 StatefullWidget 能夠創建對應的 State 對象,通過調用 state.setState() 方法觸發視圖的刷新。

state.setState() 方法內部調用了 markNeedsBuild,標記該 StatefullWidget 對應的 Element 需要刷新

_element.markNeedsBuild();

當下一個周期的幀繪制 drawFrame 時,重新調用 performRebuild(),觸發 Element 樹更新,并使用最新的 Widget 樹更新自身以及關聯的 RenderObject 對象,之后進入行布局和繪制流程。

5,Element樹如何更新?

當有 StatefullWidget 發生改變時,找到對應的 element 節點,設置它的 dirty 為 true。當下一次幀繪制 drawFrame 時,重新調用 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 內的 widget 進行比較,有4種情形,

新 widget 為空、舊 widget 也為空,返回 null

新widget為空、舊widget不為空,則移除舊child,返回null

新widget不為空、舊widget為空,創建新的Element并調用mount嵌入到樹上

新widget不為空、舊widget不為空,判斷是否可更新舊child,可以則更新child。不可以則移除舊child,創建新的Element并返回

源碼分析

我們從一個Hellow world的demo開始分析Widget源碼。

程序的入口是 runApp 方法,它傳入要顯示的界面Widget。

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

進入 runApp 方法,其中 WidgetsFlutterBinding 是一個橋接類,它是連接底層 Flutter engine SDK 的橋梁,用來接收處理 Flutter engine 傳遞過來的消息,Flutter engine 負責布局、繪制、平臺消息、手勢等功能。

WidgetsFlutterBinding 繼承自 BindingBase,BindingBase mixin 了7個類:GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding。mixin 類似于多繼承,在 mixin 模式中,后面類的同名方法會覆蓋前面類的方法。這些類組合到一塊共同監聽來的 Flutter engine 的各種消息。

WidgetsFlutterBinding 是一個單例類,ensureInitialized 方法負責初始化,返回單例對象

void runApp(Widget app) {
    WidgetsFlutterBinding.ensureInitialized()
        ..attachRootWidget(app)
        ..scheduleWarmUpFrame();
}

attachRootWidget 方法傳入 rootWidget,將 rootWidget 和 renderView(RenderObject子類)包裝到 RenderObjectToWidgetAdapter 中。renderView 是上面提到的RendererBinding 在初始化時創建,它是 RenderObject 的子類,負責實際的布局和繪制工作。

RenderObjectToWidgetAdapter 繼承自 Widget,重寫了 createElement 和 createRenderObject 方法,這兩個方法在后面構建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 方法創建并返回 RenderObjectToWidgetElement 對象,首次調用時創建新的 RenderObjectToWidgetElement 對象,再次調用復用已有的。

createElement 方法將 RenderObjectToWidgetAdapter 自身作為參數,初始化 RenderObjectToWidgetElement 對象,這樣 RenderObjectToWidgetElement 就可以取到 rootWidget 和 renderView,從而讓三者關聯起來。

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 的創建,RenderObject 也會被創建。在父類 RenderObjectElement的mount 方法中,調用 createRenderObject 得到RenderObject,這里的 widget 即是 RenderObjectToWidgetAdapter,_renderObject 是 RenderObjectToWidgetAdapter 持有的 renderView 對象

    void mount(Element parent, dynamic newSlot) {
      super.mount(parent, newSlot);  //[見小節2.5.5]
      _renderObject = widget.createRenderObject(this);
      attachRenderObject(newSlot); //將newSlot依附到RenderObject上
      _dirty = false;
    }

到這里,Element 和 RenderObject 都被創建出來?;氐?RenderObjectToWidgetElement 的 mount 方法,它調用了 _rebuild 方法,_rebuild 又調用了 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 方法中,對新舊節點的 widget 進行對比,有4種情形,
新widget為空、舊widget也為空,返回null
新widget為空、舊widget不為空,則移除舊child,返回null
新widget不為空、舊widget為空,創建新的Element并調用mount嵌入到樹上
新widget不為空、舊widget不為空,判斷是否可更新舊child,可以則更新child。不可以則移除舊child,創建新的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 從啟動到首次渲染的過程就完成了,接下來我們看看,當頁面 Widget 更新時,內部做了什么,我們從 setState() 方法開始分析

abstract class State<T extends StatefulWidget> extends Diagnosticable {
  StatefulElement _element;

  void setState(VoidCallback fn) {
    ...
    _element.markNeedsBuild(); 
  }
}

將 element.dirty 設為 true,即標記該 element 需要刷新,并將其放到臟元素數組中,等待下一個周期渲染時處理。

onBuildScheduled 是 WidgetsBinding 初始化時創建的,方法內部又調用了 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 通知頁面刷新,最終調到 drawFram 方法內的 buildScope,buildScope 首先對臟元素數組排序,淺節點靠前,深節點靠后,避免子節點先重建,父節點重建后再次重建。

  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 方法最終會調用 performRebuild(), performRebuild 又調用了 updateChild 方法,此方法在前面有介紹,在 updateChild 方法中,對新舊節點的 widget 進行對比,有4種情形,只更新需要變更的節點。

參考資料

Flutter
(一):React的設計哲學 - 簡單之美
帝國的紛爭-FlutterUI繪制解析
深入理解setState更新機制
Flutter 在銘師堂的實踐

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容