爲避免傳統的源碼講解方式的枯燥乏味,這一次,我嘗試換一種方式,帶着你以輕鬆的心態瞭解Flutter世界裏的UI繪製流程,去探究Widget、Element、RenderObject的祕密。前端
廢話很少說,聽故事!《紛爭再起》編程
十載干戈,移動端格局漸定,壁壘分明。bash
北方草原金賬王朝Javascript雖內部紛爭不斷,但卻一直窺視中原大陸,數年來襲擾不斷,現在已奪得小片領土(ReactNative)。民間盛傳:大前端融合之勢已現!markdown
2018年冬,Android邊境小城Flutter忽然宣佈立國!並對兩個移動端帝國正式宣戰!!短短几日,已攻下數城。app
而今天咱們要講的故事,就發生在戰火最嚴重的Android邊陲重鎮:View城。less
某日,Android View 城軍事會議:ide
鎮邊大將軍對手下謀士道:「Flutter 最近對咱們發起了數次進攻,已下數城,知己不知彼乃軍家大忌!誰能給我說說這個Flutter和咱們如今的View到底有什麼區別?」佈局
下方謀士面面相窺,不得已終於一個謀士站了出來:「我願意替將軍前去打探一番!」性能
很多天後,謀士:「臣臥底歸來,探明Flutter與咱們View城的主要區別在於編程範式和視圖邏輯單元不一樣」ui
將軍:「先講編程範式如何不一樣?」
將軍,咱們Android如今視圖開發是命令式的,咱們的每個View都直接遵從將軍(Developer)的指揮,例如:想要變動界面某個文案,便要指明具體TextView調用他的setText方法命令文字發生變動;
而Flutter的視圖開發是聲明式的,對方的將軍要作的是維護一套數據集,以及設定好一套布軍計劃(WidgetTree),而且爲Widget「綁定」數據集中的某個數據,根據這個數據來渲染。 例如當須要變動文案時,便改變數據集中的數據,而後直接觸發WidgetTree的從新渲染。這樣Flutter的將軍再也不須要關注每個士兵,大部分的精力都用來維護核心數據便可。
若是每一次操做都消耗一點將軍的精力值,又恰好有同一個數據「綁定」到了多個View或Widget上。命令式的編程須要作的事情是 命令N個View發生變動,消耗N點精力值;
聲明式編程須要作的事情是 變動數據+觸發WidgetTree重繪,消耗2點精力值;對精力的解放,也是Flutter能夠快速招攬到那麼多將軍的緣由之一。
將軍:」但每次數據變動,都會觸發WidgetTree的重繪,消耗的資源未免也太大了吧,我如今雖然多消耗些精力,但不會存在大量對象建立的狀況「。
謀士:這也是立刻要講的第二點不一樣。由於WidgetTree會大量的重繪,因此Widget必然是廉價的。
Flutter UI有三大元素:Widget、Element、RenderObject。對應這三者也有三個owner負責管理他們,分別是WidgetOwner(將軍&Developer)、BuildOwner、PipelineOwner。
Widget,Widget 並非真正的士兵,它只是將軍手中的棋子,是一些廉價的純對象,持有一些渲染須要的配置信息,棋子在不斷被替換着。
RenderObject,RenderObject 是真正和咱們做戰的士兵,在概念上和咱們Android的View同樣,渲染引擎會根據RenderObject來進行真正的繪製,它是相對穩定且昂貴的。
Element,使得不斷變化Widget轉變爲相對穩定的RenderObject的功臣是Element。
WidgetOwner(Developer) 在不斷改變着布軍計劃,而後向BuildOwner發送着一張又一張計劃表(WidgetTree),首次的計劃表(WidgetTree)會生成一個與之對應的ElementTree,並生成對應的RenderObjectTree。
後續BuildOwner每次收到新的計劃表就與上一次的進行對比,在ElementTree上只更新變化的部分,Element有可能僅是update一下,也有可能會被替換,Element被替換以後,與之對應的RenderObject也就被替換了。
能夠看到WidgetTree所有被替換了,但ElementTree和RenderObjectTree只替換了變化的部分。
差點忘了講 PipelineOwner, PipelineOwner相似於Android中的ViewRootImpl,管理着真正須要繪製的View, 最後PipelineOwner會對RenderObjectTree中發生變化節點的進行flush操做,最後交給底層引擎渲染。
將軍:「我大概明白了,看來保證聲明式編程性能穩定的核心在於這個Element和BuildOwner。但我看這裏還有兩個問題,RenderObject好像少了一個節點?你畫圖畫錯了嗎?還有能給我講下他是怎麼把Widget和RenderObject連接起來,以及發生變化時,BuildOwner是如何作到元素Diff的嗎?」
首先,每個Widget家族的老長輩Widget賦予了全部的Widget子類三個關鍵的能力:保證自身惟一以及定位的Key, 建立Element的 createElement, 和 canUpdate。 canUpdate 的做用後面講。
Widget子類裏還有一批特別優秀強壯的,是在紙面上表明着有渲染能力的RenderObjectWidget,它還有一個建立 RenderObject的 createRenderObject 方法。
從這裏你也看出來了,Widget、Element、RenderObject的建立關係並非線性傳遞的,Element和RenderObject都是Widget建立出來的,也並非每個Widget都有與之對應的RenderObjectWidget。這也解釋上面圖中RenderObjectTree看起來和前面的WidgetTree缺乏了一些節點。
講第一次建立,必定要從第一個被建立出來的士兵提及。咱們都知道Android的ViewTree:
-PhoneWindow
- DecorView
- TitleView
- ContentView
複製代碼
已經預先有這麼多View了,相比Android的ViewTree,Flutter的WidgetTree則要簡單的多,只有最底層的root widget。
- RenderObjectToWidgetAdapter<RenderBox>
- MyApp (自定義)
- MyMaterialApp (自定義)
複製代碼
簡單介紹一下RenderObjectToWidgetAdapter,不要被他的adapter名字迷惑了,RenderObjectToWidgetAdapter實際上是一個RenderObjectWidget,他就是第一個優秀且強壯的Widget。
這個時候就不得不搬出代碼來看了,runApp源碼:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
複製代碼
WidgetsFlutterBinding 」迷信「了一系列的Binding,這些Binding持有了咱們上面說的一些owner,好比BuildOwner,PipelineOwner,因此隨着WidgetsFlutterBinding的初始化,其餘的Binding也被初始化了,此時Flutter 的國家引擎開始轉動了!
void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement); } 複製代碼
咱們最須要關注的是attachRootWidget(app)
這個方法,這個方法很神聖,不少的第一次就在這個方法裏實現了!!(將軍:「很神聖?你是不叛變了?」),app 是咱們傳入的自定義Widget,內部會建立RenderObjectToWidgetAdapter,並將app作爲它的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; } 複製代碼
咱們解釋一下上面的圖片,Root的建立比較簡單:
attachRootWidget(app)
方法建立了Root[Widget](也就是 RenderObjectToWidgetAdapter)attachToRenderTree
方法建立了 Root[Element]mount
方法將本身掛載到父Element上,由於本身就是root了,因此沒有父Element,掛空了createRenderObject
,建立了 Root[RenderObject]它的child,也就是咱們傳入的app是怎麼掛載父控件上的呢?
owner.buildScope
,開始執行子Tree的建立以及掛載,敲黑板!!!這中間的流程和WidgetTree的刷新流程是如出一轍的,詳細流程咱們後面講!createElement
方法建立出Child[Element]mount
方法,將本身掛載到Root[Element]上,造成一棵樹widget.createRenderObject
,建立Child[RenderObject]attachRenderObject
,完成和Root[RenderObject]的連接就這樣,WidgetTree、ElementTree、RenderObject建立完成,並有各自的連接關係。
將軍:「我想看一下這個mount
和attachRenderObject
的過程,看下究竟是怎麼掛上去的」
abstract class Element: void 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);
還有兩個值得注意的地方:
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子父間的掛載。
經過前面的瞭解,咱們知道了雖然createRenderObject方法的實現是在Widget當中,但持有RenderObject引用的倒是Element。忘記啦?那咱們再看看代碼:
abstract class RenderObjectElement extends Element {
...
@override
RenderObjectWidget get widget => super.widget;
@override
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;
}
複製代碼
Element同時持有二者,能夠說,element就是Widget 和 RenderObject的中間商,它也確實在賺差價……
這個時候Root Widget,Root Element,Root RenderObject都已經建立完成而且三者連接成功。將軍您看還有什麼問題嗎?
將軍:「Flutter內部還有中間商賺差價呢?真腐敗!誒你說說他是怎麼賺差價的啊?說不定我也能夠學學~」
Flutter若是想要刷新界面,須要在StatefulWidget裏調用setState()
方法,setState()
幹啥了呢?
@protected void setState(VoidCallback fn) { ... _element.markNeedsBuild(); } 複製代碼
將軍咱們實際演練一下,假設Flutter派出了這麼一個WidgetTree:
當對方想改變下方Text Widget的文案時,會在StatefulWidget內部調用setState((){_title="ttt"})
,以後該widget對應的element將自身標記爲dirty
狀態,並調用owner.scheduleBuildFor(this);
通知buildOwner進行處理。
後續StatefulWidget的build方法必定會被執行,執行後,會建立新的子Widget出來,原來的子Widget便被拋棄掉了(將軍:「好好的一個對象就這麼被浪費了,哎……如今的年輕人~」)。
原來的子Widget確定是沒救了,但他們的Element大機率仍是有救的。
buildOwner會將全部dirty的Element添加到_dirtyElements當中,等待下一幀繪製時集中處理。
還會調用ui.window.scheduleFrame();
通知底層渲染引擎安排新的一幀處理。
這裏很重要,因此用代碼講更清晰!
void buildScope(Element context, [VoidCallback callback]){
...
}
複製代碼
buildScope!! 還記的嗎?前面講Root建立的時候,咱們就看到了Child的初次建立也是調用的buildScope方法!Tree的首幀建立和刷新是一套邏輯!
buildScope須要傳入一個Element的參數,這個方法經過字面意思咱們應該能理解,大概就是對這個Element如下(包含)的範圍rebuild。
void buildScope(Element context, [VoidCallback callback]) { ... try { ... //1.排序 _dirtyElements.sort(Element._sort); ... int dirtyCount = _dirtyElements.length; int index = 0; while (index < dirtyCount) { try { //2.遍歷rebuild _dirtyElements[index].rebuild(); } catch (e, stack) { } index += 1; } } finally { for (Element element in _dirtyElements) { element._inDirtyList = false; } //3.清空 _dirtyElements.clear(); ... } } 複製代碼
爲啥要排序呢?由於父Widget的build方法必然會觸發子Widget的build,若是先build了子Widget,後面再build父Widget時,子Widget又要被build一次。因此這樣排序以後,能夠避免子Widget的重複build。
值得一提的是,遍歷執行的過程當中,也有可能會有新的element被加入到_dirtyElements集合中,此時會根據dirtyElements集合的長度判斷是否有新的元素進來了,若是有,就從新排序。
element的rebuild方法最終會調用performRebuild()
,而performRebuild()
不一樣的Element有不一樣的實現
performRebuild()不一樣的Element有不一樣的實現,咱們暫時只看最經常使用的兩個Element:
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方法啦~
執行build方法build出來的是啥呢? 固然就是這個StatefulWidget的子Widget了。重點來了!敲黑板!!(將軍:「又給我敲黑板??」)Element就是在這個地方賺差價的!
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有四種可能的狀況:
2.若是child的widget和新build出來的同樣(Widget複用了),就看下位置同樣不,不同就更新下,同樣就直接return了。Element仍是舊的Element
3.看下Widget是否能夠update,Widget.canUpdate
的邏輯是判斷key值和運行時類型是否相等。若是知足條件的話,就更新,並返回。
中間商的差價哪來的呢?只要新build出來的Widget和上一次的類型和Key值相同,Element就會被複用!由此也就保證了雖然Widget在不停的新建,但只要不發生大的變化,那Element是相對穩定的,也就保證了RenderObject是穩定的!
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。
@override void performRebuild() { widget.updateRenderObject(this, renderObject); _dirty = false; } 複製代碼
與ComponentElement的不一樣之處在於,沒有去build,而是調用了updateRenderObject
方法更新RenderObject。
不一樣Widget也有不一樣的updateRenderObject實現,咱們看一下最經常使用的RichText,也就是Text。
void updateRenderObject(BuildContext context, RenderParagraph renderObject) { assert(textDirection != null || debugCheckHasDirectionality(context)); renderObject ..text = text ..textAlign = textAlign ..textDirection = textDirection ?? Directionality.of(context) ..softWrap = softWrap ..overflow = overflow ..textScaleFactor = textScaleFactor ..maxLines = maxLines ..locale = locale ?? Localizations.localeOf(context, nullOk: true); } 複製代碼
一些看起來比較熟悉的賦值操做,像不像Android的view呀? 要不怎麼說RenderObject實際至關於Android裏的View呢。
到這裏你基本就明白了Element是如何在中間應對Widget的多變,保障RenderObject的相對不變了吧~
在底層引擎最終回到Dart層,最終會執行WidgetsBinding 的drawFrame ()
WidgetsBinding
void drawFrame() { try { if (renderViewElement != null) buildOwner.buildScope(renderViewElement); super.drawFrame(); buildOwner.finalizeTree(); } finally { } ... } 複製代碼
buildOwner.buildScope(renderViewElement);
就是咱們上面講過的。
下面看一下super.drawFrame();
主要是PipelineOwner對RenderObject的管理,咱們簡單介紹,詳細的放在下期介紹。
@protected void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout(); //佈局須要被佈局的RenderObject pipelineOwner.flushCompositingBits(); // 判斷layer是否變化 pipelineOwner.flushPaint(); //繪製須要被繪製的RenderObject renderView.compositeFrame(); // this sends the bits to the GPU 將畫好的layer傳給engine繪製 pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. 一些語義場景須要 } 複製代碼
drawFrame方法在最後執行了buildOwner.finalizeTree();
void finalizeTree() { Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments); try { lockState(() { _inactiveElements._unmountAll(); // this unregisters the GlobalKeys }); ... } catch (e, stack) { _debugReportException('while finalizing the widget tree', e, stack); } finally { Timeline.finishSync(); } } 複製代碼
在作最後的清理工做。
將軍:「_inactiveElements」又是個啥?以前咋沒見過?
還記的前面講Element賺差價的updateChild方法嗎?全部沒用的element都調用了deactivateChild
方法進行回收:
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner._inactiveElements.add(child); // this eventually calls child.deactivate()
}
複製代碼
也就在這裏將被廢棄的element添加到了_inactiveElements當中。
另外在廢棄element以後,調用inflateWidget
建立新的element時,還調用了_retakeInactiveElement
嘗試經過GlobalKey複用element,此時的複用池也是在_inactiveElements當中。
從這裏也能瞭解到,若是你沒有在一幀裏經過GlobeKey完成Element的複用,_inactiveElements在最後將被清空,就沒辦法在複用了。
將軍,如今您對Flutter的繪製流程有了初步的瞭解了嗎?
將軍:「有些瞭解了,但你講了這麼多,對比起來咱們Android,聽起來Flutter這一套繪製流程沒啥缺點? 」
固然有了,咱們如今也只瞭解了Flutter的冰山一角,不少東西尚未發現。
但就只說動態向ViewTree中插入組件這一條,Flutter就沒有咱們靈活。由於Flutter是聲明式的,想要在運行中隨時向WidgetTree插入一個Widget,目前尚未成熟接口。
但相信隨着Flutter開發者對Flutter內部原理愈來愈熟悉,這種問題很快就會被解決的。