Flutter UI渲染分析

一、前言

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

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

二、編程範式的改變

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

三、Flutter 渲染框架介紹

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

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

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

image.png

四、視圖樹

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),咱們先從代碼入手app

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子父間的掛載。

五、渲染過程

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

六、渲染觸發 (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能夠被刪除了。

  • 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的相對不變了

七、參考

相關文章
相關標籤/搜索