Flutter的渲染流程
如果想了解flutter的渲染原理,那么flutter的三棵樹是無論如何也繞不過去的。
創建樹
創建widget樹
調用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
。最終所有Element
的RenderObject
構成一棵樹,我們稱之為Render Tree
即渲染樹
??偨Y一下,我們可以認為Flutter的UI系統包含三棵樹:Widget樹
、Element樹
、渲染樹
。他們的依賴關系是:Element樹
根據Widget樹
生成,而渲染樹
又依賴于Element樹
,最終的UI樹其實是由一個個獨立的Element
節點構成。
我習慣把三者之間的關系比作:UI設計的原型圖(Widget)、產品經理角色(Element)、開發(RenderObject):
- 相比于代碼實現,原型圖設計、更改顯得更加輕量,耗費時間成本和人力成本比較低,同時原型圖也是實際開發中必不可少的部分。
原型圖在flutter的設計理念中就好比
Widget
,它只是一個配置數據結構,創建是非常輕量的,加上flutter團隊對Widget
的創建、銷毀做了優化,不用擔心整個Widget
樹重新創建所帶來的性能問題;
- 產品經理的角色負責協調設計、開發等資源,來實現原型圖和具體的需求;
Element
同時持有Widget
和RenderObject
對象,Element
負責Widget
的渲染邏輯,同時決定要不要把RenderObject
實例attach
到Render Tree
上,只有attach
到Render Tree
上,才會被真正的渲染到屏幕上。
- 開發拿到需求,負責實現。
RenderObject
主要負責layout、paint等復雜操作,是一個真正渲染到屏幕上的View,RenderObject
和Widget
相比就不一樣了,整個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
-
StatelessWidget
:無中間狀態變化的Widget
,需要更新展示內容就得通過重新new,flutter推薦盡量使用StatelessWidget
; -
StatefullWidget
:存在中間狀態變化,那么問題來了,Widget
都是immutable的,狀態變化存儲在哪里?flutter 引入State
的類用于存放中間態,通過調用state.setState()
進行此節點及以下的整個子樹更新。
State
一個StatefulWidget
類會對應一個State
類,State
表示與其對應的StatefulWidget
要維護的狀態,State
中的保存的狀態信息可以:
- 在
Widget
構建時可以被同步讀取。 - 在
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
實例。
-
context
。StatefulWidget
對應的BuildContext
,作用同StatelessWidget
的BuildContext
。
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() { ... }
}
- initState(): state create之后被insert到tree時調用的
- didUpdateWidget(newWidget):祖先節點rebuild widget時調用
- deactivate():widget被remove的時候調用,一個widget從tree中remove掉,可以在dispose接口被調用前,重新instert到一個新tree中
- didChangeDependencies():
? 初始化時,在initState()之后立刻調用
? 當依賴的InheritedWidget rebuild,會觸發此接口被調用 - 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. - dispose():Widget徹底銷毀時調用
- reassemble(): hot reload調用
注意事項:
- 在可滾動的Widget上,當子Widget滾動出可顯示區域的時候,子Widget會被從樹中remove掉,子Widget樹中所有的state都會被dispose,state記錄的數據都會銷毀,子Widget滾動回可顯示區域時,會重新創建全新的state、element、renderobject;
- 使用hot reload功能時,要特別注意state實例是沒有重新創建的,如果該state中資源文件更新需要重啟才能生效,例如,讀取本地json文件,將數據顯示到屏幕上,修改json文件后,如果不重啟熱重載不會生效。
BuildContext
我們已經知道,StatelessWidget
和StatefulWidget
的build
方法都會傳一個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() { ... }
StatefulElement
和 StatelessElement
繼承自 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
,同時把widget
和element
賦給_state
,從而三者產生關聯關系,它的build
方法就是調用state.build(this)
,這里的this
就是StatefulElement
對象自己。
Element的生命周期:
Framework
調用Widget.createElement
創建一個Element
實例,記為element
;Framework
調用element.mount(parentElement,newSlot)
,mount
方法中首先調用element
所對應Widget
的createRenderObject
方法創建與element
相關聯的RenderObject
對象,然后調用element.attachRenderObject
方法將element.renderObject
添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發生在Element樹結構發生變化時才需要重新attach)。插入到渲染樹后的element就處于“active”狀態,處于“active”狀態后就可以顯示在屏幕上了(可以隱藏)。當有父
Widget
的配置數據改變時,同時其State.build
返回的Widget
結構與之前不同,此時就需要重新構建對應的Element
樹。為了進行Element
復用,在Element
重新構建前會先嘗試是否可以復用舊樹上相同位置的element
,element
節點在更新前都會調用其對應Widget
的canUpdate
方法,如果返回true
,則復用舊Element
,舊的Element
會使用新Widget
配置數據更新,反之則會創建一個新的Element
。Widget.canUpdate
主要是判斷newWidget
與oldWidget
的runtimeType
和key
是否同時相等,如果同時相等就返回true
,否則就會返回false
。根據這個原理,當我們需要強制更新一個Widget
時,可以通過指定不同的Key
來避免復用。當有祖先
Element
決定要移除element
時(如Widget樹結構發生了變化,導致element對應的Widget被移除),這時該祖先Element
就會調用deactivateChild
方法來移除它,移除后element.renderObject
也會被從渲染樹中移除,然后Framework
會調用element.deactivate
方法,這時element
狀態變為inactive
狀態。inactive
態的element
將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定element
,inactive
態的element
在當前動畫最后一幀結束前都會保留,如果在動畫執行結束后它還未能重新變成active
狀態,Framework
就會調用其unmount
方法將其徹底移除,這時element
的狀態為defunct
,它將永遠不會再被插入到樹中。如果
element
要重新插入到Element
樹的其它位置,如element
或element
的祖先擁有一個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
通常會通過子RenderObject
的parentData
存儲一些和子元素相關的數據,如在Stack
布局中,RenderStack
就會將子元素的偏移數據存儲在子元素的parentData
中(具體可以查看Positioned實現)。
RenderObject
類本身實現了一套基礎的layout和繪制協議,但是并沒有定義子節點模型(如一個節點可以有幾個子節點,沒有子節點?一個?兩個?或者更多?)。 它也沒有定義坐標系統(如子節點定位是在笛卡爾坐標中還是極坐標?)和具體的布局協議(是通過寬高還是通過constraint和size?,或者是否由父節點在子節點布局之前或之后設置子節點的大小和位置等)。為此,Flutter提供了一個RenderBox
類,它繼承自RenderObject
,布局坐標系統采用笛卡爾坐標系,這和Android
和iOS
原生坐標系是一致的,都是屏幕的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界面開發,這里就不贅述了。