Flutter UI渲染分析

1、前言

本篇文章主要介紹Flutter 渲染框架及其渲染過程

Flutter是谷歌的移動UI框架,在此之前也有類似ReactNative、Weex等跨端方案,Flutter在一定程度上借鑑了ReactNative的思想,採用三棵樹 其中element tree diff管理,來觸發renderTree的刷新,並且不同於android這種命令式視圖開發,採用了聲明式,下面將一一介紹。

2、編程範式的改變

在Android視圖開發中是命令式的,view大多數都是在xml聲明,開發者然後通過id找出view,數據更新時,仍需要開發者關注需要變化的view,再調用方法比如 setText之類的使其發生改變;
但是在Flutter中視圖的開發是聲明式的,開發者需要維護好一套數據集合以及綁定好widgetTree,這樣後面數據變化時候widget會根據數據來渲染,開發者就不再關注每個組件,關心核心數據即可。

3、Flutter 渲染框架介紹

Flutter的渲染框架分爲Framework和Engine兩層,應用是基於Framework層開發,其中

  • Framework層負責渲染中的Build、Layout、Paint、生成Layer等環節,使用Dart語言
  • Engine層是C++實現的渲染引擎,負責把Framework生成的Layer組合,生成紋理,然後通過OpenGL接口向GPU提交渲染數據

該跨平臺應用框架沒有使用webview或者平臺自帶的組件,使用自身的高性能渲染引擎Skia 自繪,組件之間可以任意組合
image.png

4、視圖樹

flutter中通過各種各樣的widget組合使用,視圖樹中包含了以下三種樹 Widget、Element、RenderObject,對應關係如下

image.png

  • Widget:存放渲染內容、視圖佈局信息,widget的屬性最好都是immutable
  • Element:存放上下文,通過Element遍歷視圖樹,Element同時持有Widget和RenderObject(BuilderOwner)
  • RenderObject:根據Widget的佈局屬性進行layout,paint Widget傳人的內容(PipeLineOwner)

通常 我們創建widget樹,然後調用runApp(rootWidget),將rootWidget傳給rootElement,作爲rootElement的子節點,生成Element樹,由Element樹生成Render樹
image.png

widget是immutable,數據變化會重繪,如何避免資源消耗

Flutter界面開發是一種響應式的編程,當數據發生變化時通知到可變更的節點(statefullWidget或者rootwidget),但是每次數據變更,都會觸發widgetTree的重繪,由於widget只是持有一些渲染的配置信息而已,不是真正觸發渲染的對象,非常輕量級,flutter團隊對widget的創建、銷燬做了優化,不用擔心整個widget樹重新創建帶來的性能問題。RenderObject纔是真正渲染時使用,涉及到layout、paint等複雜操作,是一個真正渲染的view,二者被Element Tree持有,ElementTree通過Diff 算法來將不斷變化的widget轉變爲相對穩定的RenderObject。
當我們不斷改變widget時,BuilderOwner收到widgetTree會與之前的widgetTree作對比,在ElementTree上只更新變化的部分,當Elment變化之後 與之對應的RenderObject也就更新了,如下圖所示
image.png可以看到WidgetTree全部被替換了,但是ElmentTree和RenderObjectTree只替換了變化的部分image.png
其中 PipelineOwner類似於Android中的ViewRootImpl,管理着真正需要繪製的View,
最後PipelineOwner會對RenderObjectTree中發生變化節點的進行layout、paint、合成等等操作,最後交給底層引擎渲染。

Widget、Element、RenderObject之間的關係

在介紹Elment Tree的Diff規則之前,先介紹下,這三者之前的關係,之前也大致提到 Elment Tree持有了Element同時持有Widget和RenderObject(BuilderOwner),我們先從代碼入手

image.png

可以看出 Widget抽象類有3個關鍵能力

  • 保證自身唯一性的key
  • 創建Element的create
  • canUpdate

從上面類圖也可以看出,**Element和RenderObject都是由Widget創建出來,**也並不是每一個Widget都有與之對應的RenderObject

Widget、Element、RenderObject 的第一次創建與關聯


在Android中ViewTree

-PhoneWindow
	- DecorView
		- TitleView
		- ContentView

而在Flutter中則比較簡單,只有底層的root widget

- RenderObjectToWidgetAdapter<RenderBox>
	- MyApp (自定義)
	- MyMaterialApp (自定義)

其中RenderObjectToWidgetAdapter 也是一個renderObjectWidget,通過註釋可以發現它是runApp啓動時“A bridge from a [RenderObject] to an [Element] tree.”
runApp代碼

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

WidgetsFlutterBinding 初始化了一系列的Binding,這些Binding持有了我們上面說的一些owner,比如BuildOwner,PipelineOwner,所以隨着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,

GestureBinding 提供了 window.onPointerDataPacket 回調,綁定 Framework 手勢子系統,是 Framework 事件模型與底層事件的綁定入口
ServicesBinding 提供了 window.onPlatformMessage 回調, 用於綁定平臺消息通道(message channel),主要處理原生和 Flutter 通信
SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回調,監聽刷新事件,綁定 Framework 繪製調度子系統
PaintingBinding 綁定繪製庫,主要用於處理圖片緩存
SemanticsBinding 語義化層與 Flutter engine 的橋樑,主要是輔助功能的底層支持
RendererBinding 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回調。它是渲染樹與 Flutter engine 的橋樑
WidgetsBinding 提供了 window.onLocaleChanged、onBuildScheduled 等回調。它是 Flutter widget 層與 engine 的橋樑

繼續跟進下attachRootWidget(app)

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
  }

內部創建了 RenderObjectToWidgetAdapter 並將我們傳入的app 自定義widget做了child,接着執行attachToRenderTree這個方法,創建了第一個Element和RenderObject

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();  //創建rootElement
        element.assignOwner(owner); //綁定BuildOwner
      });
      owner.buildScope(element, () { //子widget的初始化從這裏開始
        element.mount(null, null);  // 初始化子Widget前,先執行rootElement的mount方法
      });
    } else {
      ...
    }
    return element;
  }

image.png

我們解釋一下上面的圖片,Root的創建比較簡單:

  • 1.attachRootWidget(app) 方法創建了Root[Widget](也就是 RenderObjectToWidgetAdapter)
  • 2.緊接着調用attachToRenderTree方法創建了 Root[Element]
  • 3.Root[Element]嘗試調用mount方法將自己掛載到父Element上,因爲自己就是root了,所以沒有父Element,掛空了
  • 4.mount的過程中會調用Widget的createRenderObject,創建了 Root[RenderObject]

它的child,也就是我們傳入的app是怎麼掛載父控件上的呢?

  • 5.我們將app作爲Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了爲root[Widget]的child[Widget]
  • 6.調用owner.buildScope,開始執行子Tree的創建以及掛載,敲黑板!!!這中間的流程和WidgetTree的刷新流程是一模一樣的,詳細流程我們後面講!
  • 7.調用createElement方法創建出Child[Element]
  • 8.調用Element的mount方法,將自己掛載到Root[Element]上,形成一棵樹
  • 9.掛載的同時,調用widget.createRenderObject,創建Child[RenderObject]
  • 10.創建完成後,調用attachRenderObject,完成和Root[RenderObject]的鏈接

就這樣,WidgetTree、ElementTree、RenderObject創建完成,並有各自的鏈接關係。


這裏有兩個操作需要注意下,

mount

abstract class Element:

void mount(Element parent, dynamic newSlot) {
    _parent = parent; //持有父Element的引用
    _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; //每個Element的buildOwner,都來自父類的BuildOwner
    ...
  }

我們先看一下Element的掛載,就是讓_parent持有父Element的引用,因爲RootElement 是沒有父Element的,所以參數傳了null:element.mount(null, null);
還有兩個值得注意的地方:

  • 節點的深度_depth 也是在這個時候計算的,深度對刷新很重要
  • 每個Element的buildOwner,都來自父類的BuildOwner,這樣可以保證一個ElementTree,只由一個BuildOwner來維護。

RenderObjectElement

abstract class RenderObjectElement:

@override
  void attachRenderObject(dynamic newSlot) {
    ...
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    ...
  }

RenderObject與父RenderObject的掛載稍微複雜了點。通過代碼我們可以看到需要先查詢一下自己的AncestorRenderObject,這是爲什麼呢?
還記得之前我們講過,每一個Widget都有一個對應的Element,但Element不一定會有對應的RenderObject。所以你的父Element並不一有RenderObject,這個時候就需要向上查找。

RenderObjectElement _findAncestorRenderObjectElement() {
    Element ancestor = _parent;
    while (ancestor != null && ancestor is! RenderObjectElement)
      ancestor = ancestor._parent;
    return ancestor;
  }

通過代碼我們也可以看到,find方法在向上遍歷Element,直到找到RenderObjectElement,RenderObjectElement肯定是有對應的RenderObject了,這個時候在進行RenderObject子父間的掛載。

5、渲染過程

當需要更新UI的時候,Framework通知Engine,Engine會等到下個Vsync信號到達的時候,會通知Framework,然後Framework會進行animations,
build,layout,compositing,paint,最後生成layer提交給Engine。Engine會把layer進行組合,生成紋理,最後通過Open Gl接口提交數據給GPU,
GPU經過處理後在顯示器上面顯示。整個流程如下圖:

6、渲染觸發 (setState)

setState背後發生了什麼

image.png

在Flutter開發應用的時候,當需要更新的UI的時候,需要調用一下setState方法,然後就可以實現了UI的更新,我們接下來分析一下該方法做哪些事情。

void setState(VoidCallback fn) {
   ...
    _element.markNeedsBuild(); //通過相應的element來實現更新,關於element,widget,renderOjbect這裏不展開討論
  }

繼續追蹤

  void markNeedsBuild() {
   ...
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }

widget對應的element將自身標記爲dirty狀態,並調用owner.scheduleBuildFor(this);通知buildOwner進行處理

	void scheduleBuildFor(Element element) {
    ...
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled(); //這是一個callback,調用的方法是下面的_handleBuildScheduled
    }
    _dirtyElements.add(element); //把當前element添加到_dirtyElements數組裏面,後面重新build會遍歷這個數組
    element._inDirtyList = true;
    
  }

後續MyStatefulWidget的build方法一定會被執行,執行後,會創建新的子Widget出來,原來的子Widget便被拋棄掉了,原來的子Widget肯定是沒救了,但他們的Element大概率還是有救的,此時 buildOwner會將所有dirty的Element添加到_dirtyElements當中
經過Framework一連串的調用後,最終調用scheduleFrame來通知Engine需要更新UI,Engine就會在下個vSync到達的時候通過調用_drawFrame來通知Framework,然後Framework就會通過BuildOwner進行Build和PipelineOwner進行Layout,Paint,最後把生成Layer,組合成Scene提交給Engine。


底層引擎最終回到Dart層,並執行buildOwner的buildScope方法,首先從Engine回調Framework的入口開始。

	void _drawFrame() { //Engine回調Framework入口 
  _invoke(window.onDrawFrame, window._onDrawFrameZone);
	}

	//初始化的時候把onDrawFrame設置爲_handleDrawFrame
  void initInstances() {
    super.initInstances();
    _instance = this;
    ui.window.onBeginFrame = _handleBeginFrame;
    ui.window.onDrawFrame = _handleDrawFrame;
    SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
  }
  
  void _handleDrawFrame() {
    if (_ignoreNextEngineDrawFrame) {
      _ignoreNextEngineDrawFrame = false;
      return;
    }
    handleDrawFrame();
  }
  void handleDrawFrame() {
      _schedulerPhase = SchedulerPhase.persistentCallbacks;//記錄當前更新UI的狀態
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    }
  }

  void initInstances() {
    ....
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }

 	void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
  }

  void drawFrame() {
    ...
     if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement); //先重新build widget
      super.drawFrame();
      buildOwner.finalizeTree();
      
  }


核心方法 buildScope

void buildScope(Element context, [VoidCallback callback]){
	...
}

需要傳入一個Element的參數,這個方法通過字面意思應該理解就是對這個Element以下範圍rebuild

void buildScope(Element context, [VoidCallback callback]) {
    ...
    try {
		...
      _dirtyElements.sort(Element._sort); //1.排序
     	...
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        try {
          _dirtyElements[index].rebuild(); //2.遍歷rebuild
        } catch (e, stack) {
        }
        index += 1;
      }
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();  //3.清空
		...
    }
  }

這裏對上面方法做下解釋

  • 第1步:按照Element的深度從小到大,對_dirtyElements進行排序

由於父Widget的build方法必然會觸發子Widget的build,如果先build了子Widget,後面再build父Widget時,子Widget又要被build一次。所以這樣排序之後,可以避免子Widget的重複build。

  • 第2步:遍歷執行_dirtyElements當中element的rebuild方法

值得一提的是,遍歷執行的過程中,也有可能會有新的element被加入到_dirtyElements集合中,此時會根據dirtyElements集合的長度判斷是否有新的元素進來了,如果有,就重新排序。

element的rebuild方法最終會調用performRebuild(),而performRebuild()不同的Element有不同的實現

  • 第3步:遍歷結束之後,清空dirtyElements集合

因此setState()過程主要工作是記錄所有的髒元素,添加到BuildOwner對象的_dirtyElements成員變量,然後調用scheduleFrame來註冊Vsync回調。 當下一次vsync信號的到來時會執行handleBeginFrame()和handleDrawFrame()來更新UI。

Element的Diff

在上面的第二步會遍歷執行element的build方法
_dirtyElements[index].rebuild(); //2.遍歷rebuild
element的rebuild方法最終會調用performRebuild(),而performRebuild()不同的Element有不同的實現,以下面兩個爲例

  • ComponentElement,是StatefulWidget和StatelessElement的父類
  • RenderObjectElement, 是有渲染功能的Element的父類
ComponentElement的performRebuild()
void performRebuild() {
    Widget built;
    try {
      built = build();
    } 
    ...
    try {
      _child = updateChild(_child, built, slot);
    } 
    ...
  }

執行element的build();,以StatefulElement的build方法爲例:Widget build() => state.build(this);。 就是執行了我們複寫的StatefulWidget的state的build方法,此時創建出來的當然就是這個StatefulWidget的子Widget了

下面看下核心方法 Element updateChild(Element child, Widget newWidget, dynamic newSlot)

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
	...
		//1
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    
    if (child != null) {
    	//2
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      //3
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    //4
    return inflateWidget(newWidget, newSlot);
  }

參數child 是上一次Element掛載的child Element, newWidget 是剛剛build出來的。updateChild有四種可能的情況

  • 1.如果剛build出來的widget等於null,說明這個控件被刪除了,child Element可以被刪除了。

  • 2.如果child的widget和新build出來的一樣(Widget複用了),就看下位置一樣不,不一樣就更新下,一樣就直接return了。Element還是舊的Element

  • 3.看下Widget是否可以update,Widget.canUpdate的邏輯是判斷key值和運行時類型是否相等。如果滿足條件的話,就更新,並返回。


中間商的差價哪來的呢?只要新build出來的Widget和上一次的類型和Key值相同,Element就會被複用!由此也就保證了雖然Widget在不停的新建,但只要不發生大的變化,那Element是相對穩定的,也就保證了RenderObject是穩定的!

  • 4.如果上述三個條件都沒有滿足的話,就調用 inflateWidget() 創建新的Element

這裏再看下inflateWidget()方法:

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;
  }

首先會嘗試通過GlobalKey去查找可複用的Element,複用失敗就調用Widget的方法創建新的Element,然後調用mount方法,將自己掛載到父Element上去,mount之前我們也講過,會在這個方法裏創建新的RenderObject。

RenderObjectElement的performRebuild()
@override
  void performRebuild() {
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }

與ComponentElement的不同之處在於,沒有去build,而是調用了updateRenderObject方法更新RenderObject。到這裏我們基本就明白了Element是如何在中間應對Widget的多變,保障RenderObject的相對不變了

7、參考

發佈了170 篇原創文章 · 獲贊 143 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章