Flutter UI渲染分析

渡口一艘船發表於2020-03-04

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 Elementvoid 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經過處理後在顯示器上面顯示。整個流程如下圖:

Flutter UI渲染分析
Flutter UI渲染分析

6、渲染觸發 (setState)

setState背後發生了什麼

在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可以被刪除了。

Flutter UI渲染分析

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

Flutter UI渲染分析

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

Flutter UI渲染分析

中間商的差價哪來的呢?只要新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。

Flutter UI渲染分析

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

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

7、參考

相關文章