Flutter的渲染繪製概述

Larry就是我發表於2018-09-13

作為一個新興的跨平臺方案,Flutter在渲染方面做了什麼工作?

關於Flutter Framework的架構圖: Flutter的渲染繪製概述 Flutter的渲染繪製概述

通過上圖我們可以瞭解到Framework的底層是Flutter引擎,引擎主要負責圖形繪製(Skia)、文字排版(libtxt)和提供Dart執行時,引擎全部使用C++實現,Framework層使我們可以用Dart語言呼叫引擎的強大能力。

1.Widget

Widget是 Flutter中控制元件實現的基本單位,其意義類似於 Cocoa Touch中的 UIView。Widget裡面儲存了一個檢視的配置資訊,包括佈局、屬性等待。所以它只是一份輕量的,可直接使用的資料結構。在構建為結構樹,甚至重新建立和銷燬結構樹時都不存在明顯的效能問題。但是我們開發一般是使用Meterial 或者 Cupertino的控制元件,所以當你往檢視上新增了一個Widget後,背後支援這種渲染的是什麼?

官方對Widget的註釋分析:

/// Describes the configuration for an Element.(為Element提供配置資訊)
複製程式碼

↑ 從這一句話,隱隱感覺到Widget和Element是緊密關聯著的並且提供給Element某些資訊。

/// If you wish to associate mutable state with a widget, consider using a
/// [StatefulWidget], which creates a [State] object (via
/// [StatefulWidget.createState]) whenever it is inflated into an element and
/// incorporated into the tree.

/* 
Widget會被填充到Element,並由Element管理底層渲染樹。
Widget本身沒有可變狀態(所有的欄位必須是final)。
如果想要把可變狀態與Widget關聯起來,可以使用StatefulWidget,
StatefulWidget通過使用StatefulWidget.createState方法建立State物件,
並將之擴充到Element以及合併到樹中;	
*/
複製程式碼

↑ Widget並不會直接管理狀態及渲染,而是通過State這個物件來管理狀態。

/// A given widget can be included in the tree zero or more times. In particular
/// a given widget can be placed in the tree multiple times. Each time a widget
/// is placed in the tree, it is inflated into an [Element], which means a
/// widget that is incorporated into the tree multiple times will be inflated
/// multiple times.

/*
給定的Widget可以被包含在樹中(零次或多次)。
一個給定的Widget可以放置在樹中多次。
每次將一個Widget放入樹中時,它都會被擴充到一個Element中,
這也意味著多次併入樹中的Widget將會被多次擴充進對應的Element。
*/
複製程式碼

↑ 實際介面開發當中,一個檢視樹可能包含有多個TextWidget(widget可能被使用多次),但是這些都叫作TextWidget的widget卻被填充進一個個獨立的Element中。

/// The [key] property controls how one widget replaces another widget in the
/// tree. If the [runtimeType] and [key] properties of the two widgets are
/// [operator==], respectively, then the new widget replaces the old widget by
/// updating the underlying element (i.e., by calling [Element.update] with the
/// new widget). Otherwise, the old element is removed from the tree, the new
/// widget is inflated into an element, and the new element is inserted into the
/// tree.

/*
Widget中的Key這個屬性控制一個Widget如何替換樹中的另一個Widget。
如果兩個Widget的runtimeType和key屬性相等(==),
則新的widget通過更新Element(即通過使用新的Widget呼叫Element.update)來替換舊的Widget。
否則,如果兩個Widget的runtimeType和key屬性不相等,則舊的Element將從樹中被移除,
新的Widget將被擴充到一個新的Element中,這個新的Element將被插入樹中。
*/
複製程式碼

↑Widget通過Key和runtimeType來決定自身在Element tree中,是更新,插入,還是刪除等操作。

2.Element

開發者不用手動去操縱Element,多數情況是框架的內部邏輯在操作。

開啟Element類,在裡面發現了Element的兩個重要屬性widget和renderObject

  /// The configuration for this element.
  @override
  Widget get widget => _widget;
  Widget _widget;

  /// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element is RenderObjectElement)
        result = element.renderObject;
      else
        element.visitChildren(visit);
    }
    visit(this);
    return result;
  }
複製程式碼

↑也就是說Element同時持有了Widgets和RenderObject。那Element的作用就比較明確了,Element作為中介軟體來分離控制元件樹和渲染物件。

所以整個Element的生命週期的工作也明確了大概:

框架層通過呼叫即將被用來作為Element的初始化配置資訊的Widget的Widget.
createElement方法來建立Element; 

框架層通過呼叫mount方法來將新建立的Element新增到給定父級中給定槽點的樹上。
mount方法負責將任何Widget擴充到Widget並根據需要呼叫attachRenderObject,
以將任何關聯的渲染物件附加到渲染樹上。

此時,Element被視為“啟用的”,並可能出現在螢幕上。

在某些情況下,父(Element)可能會更改用於配置此Element的Widget,
例如因為父Element重新建立了新狀態。
發生這種情況時,框架層將呼叫新的Widget的update方法。
新Widget將始終具有與舊Widget相同的runtimeType和key屬性。
如果父Element希望在樹中的此位置更改Widget的runtimeType或key,
可以通過unmounting(解除安裝)此Element並在此位置擴充新Widget來實現。

在某些時候,祖先Element可能會決定從樹中移除該Element(或中間祖先Element),
祖先Element自己通過呼叫deactivateChild來完成該操作。
停用中間祖先將從渲染樹中移除該Element的渲染物件,
並將此Element新增到owner屬性中的非活動元素列表中,
從而讓框架層呼叫deactivate方法作用在此Element上。

此時,該Element被視為“無效狀態”,
並且不會出現在螢幕上。一個Element可以保持非活動狀態,
直到當前動畫幀結束。在動畫幀結束時,任何仍處於非活動狀態的Element都將被解除安裝。

如果Element被重新組合到樹中(例如,因為它或其祖先之一有一個全域性鍵(global key)被重用),
框架層將從owner屬性中的非活動Element列表中移除該Element,
並呼叫該Element的activate方法,
並重新附加Element的渲染物件到渲染樹。 
(此時,Element再次被視為“活動狀態”並可能出現在螢幕上。)

如果Element在當前動畫幀的末尾沒有被重新組合到樹中,則框架層將呼叫該元素的unmount方法。

此時,該元素被視為“已停用”,並且將來不會併入樹中。
複製程式碼

3.RenderObject

官方定義:An object in the render tree.渲染樹中的一個物件。從其名字,我們可以很直觀地知道,它就是負責渲染的工作,實際上,所有的佈局、繪製和事件響應全都由它負責,開發複雜檢視時我們可能經常需要與之打交道。

而它又是由 Element 的子類 RenderObjectElement 建立出來的,RenderObject 也會構成一個 Render Tree,並且每個 RenderObject 也都會被儲存下來以便在更新時複用,Render Tree構建的資料會被加入到 Engine所需的 LayerTree中,Engine通過 LayerTree進行檢視合成並光柵化,提交給 GPU。

1,2,3所構成的Tree很像iOS的模型樹-->呈現樹-->渲染樹

4.Layout

當前面的準備工作完成後,具體展示在螢幕上時就需要佈局。佈局可以計算出每個節點所佔空間的真實大小。在構建檢視樹時,節點的 size約束是從樹頂部向底部的,而在計算佈局的時候,遍歷樹是深度優先,所以獲得佈局資料的順序是自下而上的。和 Web一樣,Flutter的佈局也是基於盒子模型。

5.Painting

在進行繪製時,每個節點都會先繪製自身,其次才是子節點。比如節點 2是一個背景色綠色的檢視,在繪製完自身後,繪製子節點 3和 4,它們可能具有 alpha屬性,所以繪製後當佈局重疊時會合成紅色的節點 5。所以從資料流方向看的時候,獲得最終的 Layer的順序反而是自下而上的。

Flutter的渲染繪製概述

Flutter使用邊界標記需要重新佈局和重新繪製的節點部分,在進入和走出重繪邊界時,Flutter會強制切換新的圖層,這樣就可以避免其他節點被汙染或者觸發重建,這個邊界被分別叫做 Relayout boundary 和 Repaint boundary。

Flutter的渲染繪製概述

總結:

所以每當我使用了一個Widget的時候,它在Flutter介面渲染過程經歷了三個階段:佈局、繪製、合成,佈局和繪製在Flutter框架中完成,合成則交由引擎負責。

Flutter的渲染繪製概述

參考資料:

Flutter view basic introduction Widget, Element, RenderObject

Flutter原理與實踐

Flutter 渲染流水線淺析

Flutter 中文網

Flutter's Rendering Pipeline

相關文章