Flutter的渲染流程

如果想了解flutter的渲染原理,那么flutter的三棵樹是無論如何也繞不過去的。
創(chuàng)建樹
創(chuàng)建widget樹
調(diào)用runApp(rootWidget),將rootWidget傳給rootElement,做為rootElement的子節(jié)點,生成Element樹,由Element樹生成Render樹
Widget:存放渲染內(nèi)容、視圖布局信息,widget的屬性最好都是immutable(如何更新數(shù)據(jù)呢?查看后續(xù)內(nèi)容)
Element:存放上下文,通過Element遍歷視圖樹,Element同時持有Widget和RenderObject
RenderObject:根據(jù)Widget的布局屬性進行l(wèi)ayout,paint Widget傳人的內(nèi)容

從創(chuàng)建到渲染的大體流程是:根據(jù)Widget生成Element,然后創(chuàng)建相應(yīng)的RenderObject并關(guān)聯(lián)到Element.renderObject屬性上,最后再通過RenderObject來完成布局排列和繪制。Element就是Widget在UI樹具體位置的一個實例化對象,大多數(shù)Element只有唯一的renderObject,但還有一些Element會有多個子節(jié)點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有Element的RenderObject構(gòu)成一棵樹,我們稱之為Render Tree即渲染樹。總結(jié)一下,我們可以認為Flutter的UI系統(tǒng)包含三棵樹:Widget樹、Element樹、渲染樹。他們的依賴關(guān)系是:Element樹根據(jù)Widget樹生成,而渲染樹又依賴于Element樹,最終的UI樹其實是由一個個獨立的Element節(jié)點構(gòu)成。
我習慣把三者之間的關(guān)系比作:UI設(shè)計的原型圖(Widget)、產(chǎn)品經(jīng)理角色(Element)、開發(fā)(RenderObject):
- 相比于代碼實現(xiàn),原型圖設(shè)計、更改顯得更加輕量,耗費時間成本和人力成本比較低,同時原型圖也是實際開發(fā)中必不可少的部分。
原型圖在flutter的設(shè)計理念中就好比
Widget,它只是一個配置數(shù)據(jù)結(jié)構(gòu),創(chuàng)建是非常輕量的,加上flutter團隊對Widget的創(chuàng)建、銷毀做了優(yōu)化,不用擔心整個Widget樹重新創(chuàng)建所帶來的性能問題;
- 產(chǎn)品經(jīng)理的角色負責協(xié)調(diào)設(shè)計、開發(fā)等資源,來實現(xiàn)原型圖和具體的需求;
Element同時持有Widget和RenderObject對象,Element負責Widget的渲染邏輯,同時決定要不要把RenderObject實例attach到Render Tree上,只有attach到Render Tree上,才會被真正的渲染到屏幕上。
- 開發(fā)拿到需求,負責實現(xiàn)。
RenderObject主要負責layout、paint等復雜操作,是一個真正渲染到屏幕上的View,RenderObject和Widget相比就不一樣了,整個RenderObject 樹重新創(chuàng)建開銷就比較大,所以當Widget重新創(chuàng)建,Element樹和RenderObject樹并不會完全重新創(chuàng)建。
通過這個簡單的比喻,flutter渲染的三棵樹是不是就比較容易理解了,接下來我們再來看看它的具體實現(xiàn)。
Widget
在Flutter中,幾乎所有的對象都是一個Widget。與原生開發(fā)中 “控件” 不同的是,Flutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector、用于APP主題數(shù)據(jù)傳遞的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:無中間狀態(tài)變化的Widget,需要更新展示內(nèi)容就得通過重新new,flutter推薦盡量使用StatelessWidget; -
StatefullWidget:存在中間狀態(tài)變化,那么問題來了,Widget都是immutable的,狀態(tài)變化存儲在哪里?flutter 引入State的類用于存放中間態(tài),通過調(diào)用state.setState()進行此節(jié)點及以下的整個子樹更新。
State
一個StatefulWidget類會對應(yīng)一個State類,State表示與其對應(yīng)的StatefulWidget要維護的狀態(tài),State中的保存的狀態(tài)信息可以:
- 在
Widget構(gòu)建時可以被同步讀取。 - 在
Widget生命周期中可以被改變,當State被改變時,可以手動調(diào)用其setState()方法通知Flutter framework狀態(tài)發(fā)生改變,Flutter framework在收到消息后,會重新調(diào)用其build方法重新構(gòu)建Widget樹,從而達到更新UI的目的。
State中有兩個常用屬性:
1.Widget,它表示與該State實例關(guān)聯(lián)的Widget實例,由Flutter framework動態(tài)設(shè)置。注意,這種關(guān)聯(lián)并非永久的,因為在應(yīng)用生命周期中,UI樹上的某一個節(jié)點的Widget實例在重新構(gòu)建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建,當在重新構(gòu)建時,如果Widget被修改了,Flutter framework會動態(tài)設(shè)置State.widget為新的Widget實例。
-
context。StatefulWidget對應(yīng)的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時調(diào)用的
- didUpdateWidget(newWidget):祖先節(jié)點rebuild widget時調(diào)用
- deactivate():widget被remove的時候調(diào)用,一個widget從tree中remove掉,可以在dispose接口被調(diào)用前,重新instert到一個新tree中
- didChangeDependencies():
? 初始化時,在initState()之后立刻調(diào)用
? 當依賴的InheritedWidget rebuild,會觸發(fā)此接口被調(diào)用 - 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徹底銷毀時調(diào)用
- reassemble(): hot reload調(diào)用
注意事項:
- 在可滾動的Widget上,當子Widget滾動出可顯示區(qū)域的時候,子Widget會被從樹中remove掉,子Widget樹中所有的state都會被dispose,state記錄的數(shù)據(jù)都會銷毀,子Widget滾動回可顯示區(qū)域時,會重新創(chuàng)建全新的state、element、renderobject;
- 使用hot reload功能時,要特別注意state實例是沒有重新創(chuàng)建的,如果該state中資源文件更新需要重啟才能生效,例如,讀取本地json文件,將數(shù)據(jù)顯示到屏幕上,修改json文件后,如果不重啟熱重載不會生效。
BuildContext
我們已經(jīng)知道,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到底是什么呢,查看其定義,發(fā)現(xiàn)其是一個抽象接口類:
abstract class BuildContext {
Widget get widget;
...
}
還記得Widget抽象類中的createElement方法嗎?你是不是已經(jīng)猜到了?沒錯,Widget build(BuildContext context) 中的 BuildContext就是 Element的實例。
Element
查看Element定義,發(fā)現(xiàn)它也是一個抽象類:
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);
...
}
在創(chuàng)建StatefulElement實例時,會調(diào)用widget.createState()賦給私有變量_state,同時把widget和element賦給_state,從而三者產(chǎn)生關(guān)聯(lián)關(guān)系,它的build方法就是調(diào)用state.build(this),這里的this就是StatefulElement對象自己。
Element的生命周期:
Framework調(diào)用Widget.createElement創(chuàng)建一個Element實例,記為element;Framework調(diào)用element.mount(parentElement,newSlot),mount方法中首先調(diào)用element所對應(yīng)Widget的createRenderObject方法創(chuàng)建與element相關(guān)聯(lián)的RenderObject對象,然后調(diào)用element.attachRenderObject方法將element.renderObject添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發(fā)生在Element樹結(jié)構(gòu)發(fā)生變化時才需要重新attach)。插入到渲染樹后的element就處于“active”狀態(tài),處于“active”狀態(tài)后就可以顯示在屏幕上了(可以隱藏)。當有父
Widget的配置數(shù)據(jù)改變時,同時其State.build返回的Widget結(jié)構(gòu)與之前不同,此時就需要重新構(gòu)建對應(yīng)的Element樹。為了進行Element復用,在Element重新構(gòu)建前會先嘗試是否可以復用舊樹上相同位置的element,element節(jié)點在更新前都會調(diào)用其對應(yīng)Widget的canUpdate方法,如果返回true,則復用舊Element,舊的Element會使用新Widget配置數(shù)據(jù)更新,反之則會創(chuàng)建一個新的Element。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時相等,如果同時相等就返回true,否則就會返回false。根據(jù)這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。當有祖先
Element決定要移除element時(如Widget樹結(jié)構(gòu)發(fā)生了變化,導致element對應(yīng)的Widget被移除),這時該祖先Element就會調(diào)用deactivateChild方法來移除它,移除后element.renderObject也會被從渲染樹中移除,然后Framework會調(diào)用element.deactivate方法,這時element狀態(tài)變?yōu)?code>inactive狀態(tài)。inactive態(tài)的element將不會再顯示到屏幕。為了避免在一次動畫執(zhí)行過程中反復創(chuàng)建、移除某個特定element,inactive態(tài)的element在當前動畫最后一幀結(jié)束前都會保留,如果在動畫執(zhí)行結(jié)束后它還未能重新變成active狀態(tài),Framework就會調(diào)用其unmount方法將其徹底移除,這時element的狀態(tài)為defunct,它將永遠不會再被插入到樹中。如果
element要重新插入到Element樹的其它位置,如element或element的祖先擁有一個GlobalKey(用于全局復用元素),那么Framework會先將element從現(xiàn)有位置移除,然后再調(diào)用其activate方法,并將其renderObject重新attach到渲染樹。
看完Element的生命周期,可能有些讀者會有疑問,開發(fā)者會直接操作Element樹嗎?其實對于開發(fā)者來說,大多數(shù)情況下只需要關(guān)注Widget樹就行,F(xiàn)lutter框架已經(jīng)將對Widget樹的操作映射到了Element樹上,這可以極大的降低復雜度,提高開發(fā)效率。但是了解Element對理解整個Flutter UI框架是至關(guān)重要的,F(xiàn)lutter正是通過Element這個紐帶將Widget和RenderObject關(guān)聯(lián)起來,了解Element層不僅會幫助讀者對Flutter UI框架有個清晰的認識,而且也會提高自己的抽象能力和設(shè)計能力。
RenderObject
我們說過每個Element都對應(yīng)一個RenderObject,我們可以通過Element.renderObject 來獲取。并且我們也說過RenderObject的主要職責是Layout和繪制,所有的RenderObject會組成一棵渲染樹Render Tree。RenderObject就是渲染樹中的一個對象,它擁有一個parent和一個parentData 插槽(slot),所謂插槽,就是指預留的一個接口或位置,這個接口和位置是由其它對象來接入或占據(jù)的,這個接口或位置在軟件中通常用預留變量來表示,而parentData正是一個預留變量,它正是由parent 來賦值的,parent通常會通過子RenderObject的parentData存儲一些和子元素相關(guān)的數(shù)據(jù),如在Stack布局中,RenderStack就會將子元素的偏移數(shù)據(jù)存儲在子元素的parentData中(具體可以查看Positioned實現(xiàn))。
RenderObject類本身實現(xiàn)了一套基礎(chǔ)的layout和繪制協(xié)議,但是并沒有定義子節(jié)點模型(如一個節(jié)點可以有幾個子節(jié)點,沒有子節(jié)點?一個?兩個?或者更多?)。 它也沒有定義坐標系統(tǒng)(如子節(jié)點定位是在笛卡爾坐標中還是極坐標?)和具體的布局協(xié)議(是通過寬高還是通過constraint和size?,或者是否由父節(jié)點在子節(jié)點布局之前或之后設(shè)置子節(jié)點的大小和位置等)。為此,F(xiàn)lutter提供了一個RenderBox類,它繼承自RenderObject,布局坐標系統(tǒng)采用笛卡爾坐標系,這和Android和iOS原生坐標系是一致的,都是屏幕的top、left是原點,然后分寬高兩個軸。
我們知道 StatelessWidget 和 StatefulWidget 兩種直接繼承自 Widget 的類,在 Flutter 中,還有另一個類 RenderObjectWidget 也同樣直接繼承自 Widget,它沒有 build 方法,可通過 createRenderObject 直接創(chuàng)建 RenderObject 對象放入渲染樹中。Column 和 Row 等控件都間接繼承自 RenderObjectWidget。
主要屬性和方法如下:
- constraints 對象,從其父級傳遞給它的約束
- parentData 對象,其父對象附加有用的信息。
- performLayout 方法,計算此渲染對象的布局。
- paint 方法,繪制該組件及其子組件。
RenderObject 作為一個抽象類。每個節(jié)點需要實現(xiàn)它才能進行實際渲染。擴展 RenderOject 的兩個最重要的類是RenderBox 和 RenderSliver。這兩個類分別是應(yīng)用了 Box 協(xié)議和 Sliver 協(xié)議這兩種布局協(xié)議的所有渲染對象的父類,其還擴展了數(shù)十個和其他幾個處理特定場景的類,并實現(xiàn)了渲染過程的細節(jié),如 RenderShiftedBox 和 RenderStack 等等。
RenderObject具體如何布局以及Size、Offset的計算方式可以查閱咸魚的技術(shù)文章深入了解Flutter界面開發(fā),這里就不贅述了。