flutter 渲染三棵樹(Widget、Element、RenderObject)

Flutter的渲染流程

如果想了解flutter的渲染原理,那么flutter的三棵樹是無論如何也繞不過去的。

創建樹
  1. 創建widget樹

  2. 調用runApp(rootWidget),將rootWidget傳給rootElement,做為rootElement的子節點,生成Element樹,由Element樹生成Render樹

  • Widget:存放渲染內容、視圖布局信息,widget的屬性最好都是immutable(如何更新數據呢?查看后續內容)

  • Element:存放上下文,通過Element遍歷視圖樹,Element同時持有Widget和RenderObject

  • RenderObject:根據Widget的布局屬性進行layout,paint Widget傳人的內容

三棵樹

從創建到渲染的大體流程是:根據Widget生成Element,然后創建相應的RenderObject并關聯到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。Element就是Widget在UI樹具體位置的一個實例化對象,大多數Element只有唯一的renderObject,但還有一些Element會有多個子節點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有ElementRenderObject構成一棵樹,我們稱之為Render Tree渲染樹??偨Y一下,我們可以認為Flutter的UI系統包含三棵樹:Widget樹Element樹、渲染樹。他們的依賴關系是:Element樹根據Widget樹生成,而渲染樹又依賴于Element樹,最終的UI樹其實是由一個個獨立的Element節點構成。

我習慣把三者之間的關系比作:UI設計的原型圖(Widget)、產品經理角色(Element)、開發(RenderObject):

  • 相比于代碼實現,原型圖設計、更改顯得更加輕量,耗費時間成本和人力成本比較低,同時原型圖也是實際開發中必不可少的部分。

原型圖在flutter的設計理念中就好比Widget,它只是一個配置數據結構,創建是非常輕量的,加上flutter團隊對Widget的創建、銷毀做了優化,不用擔心整個Widget樹重新創建所帶來的性能問題;

  • 產品經理的角色負責協調設計、開發等資源,來實現原型圖和具體的需求;

Element 同時持有 WidgetRenderObject 對象,Element 負責 Widget 的渲染邏輯,同時決定要不要把 RenderObject 實例 attachRender Tree 上,只有 attachRender Tree 上,才會被真正的渲染到屏幕上。

  • 開發拿到需求,負責實現。

RenderObject主要負責layout、paint等復雜操作,是一個真正渲染到屏幕上的View,RenderObjectWidget 相比就不一樣了,整個 RenderObject 樹 重新創建開銷就比較大,所以當Widget重新創建,Element樹和RenderObject樹并不會完全重新創建。

通過這個簡單的比喻,flutter渲染的三棵樹是不是就比較容易理解了,接下來我們再來看看它的具體實現。

Widget

Flutter中,幾乎所有的對象都是一個Widget。與原生開發中 “控件” 不同的是,Flutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector、用于APP主題數據傳遞的Theme、布局元素等等。

@immutable
abstract class Widget extends DiagnosticableTree {

  const Widget({ this.key });

  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
StatelessWidget 和 StatefulWidget
  1. StatelessWidget:無中間狀態變化的Widget,需要更新展示內容就得通過重新new,flutter推薦盡量使用StatelessWidget;
  2. StatefullWidget:存在中間狀態變化,那么問題來了,Widget都是immutable的,狀態變化存儲在哪里?flutter 引入State的類用于存放中間態,通過調用state.setState()進行此節點及以下的整個子樹更新。

State

一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態,State中的保存的狀態信息可以:

  1. Widget 構建時可以被同步讀取。
  2. Widget 生命周期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到消息后,會重新調用其build方法重新構建Widget樹,從而達到更新UI的目的。
State中有兩個常用屬性:

1.Widget,它表示與該State實例關聯的Widget實例,由Flutter framework動態設置。注意,這種關聯并非永久的,因為在應用生命周期中,UI樹上的某一個節點的Widget實例在重新構建時可能會變化,但State實例只會在第一次插入到樹中時被創建,當在重新構建時,如果Widget被修改了,Flutter framework會動態設置State.widget為新的Widget實例。

  1. context。StatefulWidget對應的BuildContext,作用同StatelessWidgetBuildContext
State的生命周期
abstract class State<T extends StatefulWidget> extends Diagnosticable {

  T get widget => _widget;
  T _widget;

  BuildContext get context => _element;
  StatefulElement _element;

  @protected
  @mustCallSuper
  void initState() { ... }

  @protected
  @mustCallSuper
  void reassemble() { ... }

  @protected
  void setState(VoidCallback fn) {
    // 省略掉一些邏輯判斷
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

  @protected
  @mustCallSuper
  void deactivate() { ... }

  @protected
  @mustCallSuper
  void dispose() { ... }

  @protected
  Widget build(BuildContext context);

  @protected
  @mustCallSuper
  void didChangeDependencies() { ... }
}
  1. initState(): state create之后被insert到tree時調用的
  2. didUpdateWidget(newWidget):祖先節點rebuild widget時調用
  3. deactivate():widget被remove的時候調用,一個widget從tree中remove掉,可以在dispose接口被調用前,重新instert到一個新tree中
  4. didChangeDependencies():
    ? 初始化時,在initState()之后立刻調用
    ? 當依賴的InheritedWidget rebuild,會觸發此接口被調用
  5. build():
    ? After calling [initState].
    ? After calling [didUpdateWidget].
    ? After receiving a call to [setState].
    ? After a dependency of this [State] object changes (e.g., an[InheritedWidget] referenced by the previous [build] changes).
    ? After calling [deactivate] and then reinserting the [State] object into the tree at another location.
  6. dispose():Widget徹底銷毀時調用
  7. reassemble(): hot reload調用

注意事項:

  1. 在可滾動的Widget上,當子Widget滾動出可顯示區域的時候,子Widget會被從樹中remove掉,子Widget樹中所有的state都會被dispose,state記錄的數據都會銷毀,子Widget滾動回可顯示區域時,會重新創建全新的state、element、renderobject;
  2. 使用hot reload功能時,要特別注意state實例是沒有重新創建的,如果該state中資源文件更新需要重啟才能生效,例如,讀取本地json文件,將數據顯示到屏幕上,修改json文件后,如果不重啟熱重載不會生效。
BuildContext

我們已經知道,StatelessWidgetStatefulWidgetbuild方法都會傳一個BuildContext對象:

Widget build(BuildContext context) {}

在很多時候我們都需要使用這個context 做一些事,比如:

Theme.of(context) //獲取主題
Navigator.push(context, route) //入棧新路由
Localizations.of(context, type) //獲取Local
context.size //獲取上下文大小
context.findRenderObject() //查找當前或最近的一個祖先RenderObject

那么BuildContext到底是什么呢,查看其定義,發現其是一個抽象接口類:

abstract class BuildContext {
  Widget get widget;
  ...
}

還記得Widget抽象類中的createElement方法嗎?你是不是已經猜到了?沒錯,Widget build(BuildContext context) 中的 BuildContext就是 Element的實例。

Element

查看Element定義,發現它也是一個抽象類:

abstract class Element extends DiagnosticableTree implements BuildContext {
  
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

  Element _parent;

  @override
  Widget get widget => _widget;
  Widget _widget;

  RenderObject get renderObject { ... }

  @mustCallSuper
  void mount(Element parent, dynamic newSlot) { ... }

  @mustCallSuper
  void activate() { ... }

  @mustCallSuper
  void deactivate() { ... }

  @mustCallSuper
  void unmount() { ... }

StatefulElementStatelessElement 繼承自 ComponentElement, ComponentElement 是繼承自 Element 的抽象類:

abstract class ComponentElement extends Element { ... }

StatefulElement為例:

class StatefulElement extends ComponentElement {
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    ...
    _state._element = this;
    _state._widget = widget;
    ...
  }

  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
  ...
  @override
  Widget build() => state.build(this);
  ...
}

在創建StatefulElement實例時,會調用widget.createState()賦給私有變量_state,同時把widgetelement賦給_state,從而三者產生關聯關系,它的build方法就是調用state.build(this),這里的this就是StatefulElement對象自己。

Element的生命周期:
  1. Framework 調用Widget.createElement 創建一個Element實例,記為element;

  2. Framework 調用 element.mount(parentElement,newSlot) ,mount方法中首先調用element所對應WidgetcreateRenderObject方法創建與element相關聯的RenderObject對象,然后調用element.attachRenderObject方法將element.renderObject添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發生在Element樹結構發生變化時才需要重新attach)。插入到渲染樹后的element就處于“active”狀態,處于“active”狀態后就可以顯示在屏幕上了(可以隱藏)。

  3. 當有父Widget的配置數據改變時,同時其State.build返回的Widget結構與之前不同,此時就需要重新構建對應的Element樹。為了進行Element復用,在Element重新構建前會先嘗試是否可以復用舊樹上相同位置的elementelement節點在更新前都會調用其對應WidgetcanUpdate方法,如果返回true,則復用舊Element,舊的Element會使用新Widget配置數據更新,反之則會創建一個新的ElementWidget.canUpdate主要是判斷newWidgetoldWidgetruntimeTypekey是否同時相等,如果同時相等就返回true,否則就會返回false。根據這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。

  4. 當有祖先Element決定要移除element 時(如Widget樹結構發生了變化,導致element對應的Widget被移除),這時該祖先Element就會調用deactivateChild 方法來移除它,移除后element.renderObject也會被從渲染樹中移除,然后Framework會調用element.deactivate 方法,這時element狀態變為inactive狀態。

  5. inactive態的element將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定elementinactive態的element在當前動畫最后一幀結束前都會保留,如果在動畫執行結束后它還未能重新變成active狀態,Framework就會調用其unmount方法將其徹底移除,這時element的狀態為defunct,它將永遠不會再被插入到樹中。

  6. 如果element要重新插入到Element樹的其它位置,如elementelement的祖先擁有一個GlobalKey(用于全局復用元素),那么Framework會先將element從現有位置移除,然后再調用其activate方法,并將其renderObject重新attach到渲染樹。

看完Element的生命周期,可能有些讀者會有疑問,開發者會直接操作Element樹嗎?其實對于開發者來說,大多數情況下只需要關注Widget樹就行,Flutter框架已經將對Widget樹的操作映射到了Element樹上,這可以極大的降低復雜度,提高開發效率。但是了解Element對理解整個Flutter UI框架是至關重要的,Flutter正是通過Element這個紐帶將Widget和RenderObject關聯起來,了解Element層不僅會幫助讀者對Flutter UI框架有個清晰的認識,而且也會提高自己的抽象能力和設計能力。

RenderObject

我們說過每個Element都對應一個RenderObject,我們可以通過Element.renderObject 來獲取。并且我們也說過RenderObject的主要職責是Layout和繪制,所有的RenderObject會組成一棵渲染樹Render Tree。RenderObject就是渲染樹中的一個對象,它擁有一個parent和一個parentData 插槽(slot),所謂插槽,就是指預留的一個接口或位置,這個接口和位置是由其它對象來接入或占據的,這個接口或位置在軟件中通常用預留變量來表示,而parentData正是一個預留變量,它正是由parent 來賦值的,parent通常會通過子RenderObjectparentData存儲一些和子元素相關的數據,如在Stack布局中,RenderStack就會將子元素的偏移數據存儲在子元素的parentData中(具體可以查看Positioned實現)。

RenderObject類本身實現了一套基礎的layout和繪制協議,但是并沒有定義子節點模型(如一個節點可以有幾個子節點,沒有子節點?一個?兩個?或者更多?)。 它也沒有定義坐標系統(如子節點定位是在笛卡爾坐標中還是極坐標?)和具體的布局協議(是通過寬高還是通過constraint和size?,或者是否由父節點在子節點布局之前或之后設置子節點的大小和位置等)。為此,Flutter提供了一個RenderBox類,它繼承自RenderObject,布局坐標系統采用笛卡爾坐標系,這和AndroidiOS原生坐標系是一致的,都是屏幕的top、left是原點,然后分寬高兩個軸。

我們知道 StatelessWidget 和 StatefulWidget 兩種直接繼承自 Widget 的類,在 Flutter 中,還有另一個類 RenderObjectWidget 也同樣直接繼承自 Widget,它沒有 build 方法,可通過 createRenderObject 直接創建 RenderObject 對象放入渲染樹中。Column 和 Row 等控件都間接繼承自 RenderObjectWidget。

主要屬性和方法如下:

  • constraints 對象,從其父級傳遞給它的約束
  • parentData 對象,其父對象附加有用的信息。
  • performLayout 方法,計算此渲染對象的布局。
  • paint 方法,繪制該組件及其子組件。

RenderObject 作為一個抽象類。每個節點需要實現它才能進行實際渲染。擴展 RenderOject 的兩個最重要的類是RenderBox 和 RenderSliver。這兩個類分別是應用了 Box 協議和 Sliver 協議這兩種布局協議的所有渲染對象的父類,其還擴展了數十個和其他幾個處理特定場景的類,并實現了渲染過程的細節,如 RenderShiftedBox 和 RenderStack 等等。

RenderObject具體如何布局以及Size、Offset的計算方式可以查閱咸魚的技術文章深入了解Flutter界面開發,這里就不贅述了。

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容