為避免傳統的原始碼講解方式的枯燥乏味,這一次,我嘗試換一種方式,帶著你以輕鬆的心態瞭解Flutter世界裡的UI繪製流程,去探究Widget、Element、RenderObject的祕密。
廢話不多說,聽故事!《紛爭再起》
故事
十載干戈,移動端格局漸定,壁壘分明。
北方草原金帳王朝Javascript雖內部紛爭不斷,但卻一直窺視中原大陸,數年來襲擾不斷,如今已奪得小片領土(ReactNative)。民間盛傳:大前端融合之勢已現!
2018年冬,Android邊境小城Flutter突然宣佈立國!並對兩個移動端帝國正式宣戰!!短短几日,已攻下數城。
而今天我們要講的故事,就發生在戰火最嚴重的Android邊陲重鎮:View城。
某日,Android View 城軍事會議:
鎮邊大將軍對手下謀士道:“Flutter 最近對我們發起了數次進攻,已下數城,知己不知彼乃軍家大忌!誰能給我說說這個Flutter和我們現在的View到底有什麼區別?”
下方謀士面面相窺,不得已終於一個謀士站了出來:“我願意替將軍前去打探一番!”
數日後,謀士:“臣臥底歸來,探明Flutter與我們View城的主要區別在於程式設計正規化和檢視邏輯單元不同”
將軍:“先講程式設計正規化如何不同?”
Android/Flutter 程式設計正規化
將軍,我們Android現在檢視開發是命令式的,我們的每一個View都直接聽從將軍(Developer)的指揮,例如:想要變更介面某個文案,便要指明具體TextView呼叫他的setText方法命令文字發生變更;
而Flutter的檢視開發是宣告式的,對方的將軍要做的是維護一套資料集,以及設定好一套布軍計劃(WidgetTree),並且為Widget“繫結”資料集中的某個資料,根據這個資料來渲染。 例如當需要變更文案時,便改變資料集中的資料,然後直接觸發WidgetTree的重新渲染。這樣Flutter的將軍不再需要關注每一個士兵,大部分的精力都用來維護核心資料即可。
如果每一次操作都消耗一點將軍的精力值,又剛好有同一個資料“繫結”到了多個View或Widget上。命令式的程式設計需要做的事情是 命令N個View發生變更,消耗N點精力值;
宣告式程式設計需要做的事情是 變更資料+觸發WidgetTree重繪,消耗2點精力值;對精力的解放,也是Flutter可以快速招攬到那麼多將軍的原因之一。
將軍:”但每次資料變更,都會觸發WidgetTree的重繪,消耗的資源未免也太大了吧,我現在雖然多消耗些精力,但不會存在大量物件建立的情況“。
Widget、Element、RenderObject概念
謀士:這也是馬上要講的第二點不同。因為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、Element、RenderObject之間的關係
首先,每一個Widget家族的老長輩Widget賦予了所有的Widget子類三個關鍵的能力:保證自身唯一以及定位的Key, 建立Element的 createElement, 和 canUpdate。 canUpdate 的作用後面講。
Widget子類裡還有一批特別優秀強壯的,是在紙面上代表著有渲染能力的RenderObjectWidget,它還有一個建立 RenderObject的 createRenderObject 方法。
從這裡你也看出來了,Widget、Element、RenderObject的建立關係並不是線性傳遞的,Element和RenderObject都是Widget建立出來的,也並不是每一個Widget都有與之對應的RenderObjectWidget。這也解釋上面圖中RenderObjectTree看起來和前面的WidgetTree缺少了一些節點。
Widget、Element、RenderObject 的第一次建立與關聯
講第一次建立,一定要從第一個被建立出來的士兵說起。我們都知道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的建立比較簡單:
- 1.
attachRootWidget(app)
方法建立了Root[Widget](也就是 RenderObjectToWidgetAdapter) - 2.緊接著呼叫
attachToRenderTree
方法建立了 Root[Element] - 3.Root[Element]嘗試呼叫
mount
方法將自己掛載到父Element上,因為自己就是root了,所以沒有父Element,掛空了 - 4.mount的過程中會呼叫Widget的
createRenderObject
,建立了 Root[RenderObject]
它的child,也就是我們傳入的app是怎麼掛載父控制元件上的呢?
- 5.我們將app作為引數傳給了Root[Widget](也就是 RenderObjectToWidgetAdapter),app[Widget]也就成了為root[Widget]的child[Widget]
- 6.呼叫
owner.buildScope
,開始執行子Tree的建立以及掛載,敲黑板!!!這中間的流程和WidgetTree的重新整理流程是一模一樣的,詳細流程我們後面講! - 7.呼叫
createElement
方法建立出Child[Element] - 8.呼叫Element的
mount
方法,將自己掛載到Root[Element]上,形成一棵樹 - 9.掛載的同時,呼叫
widget.createRenderObject
,建立Child[RenderObject] - 10.建立完成後,呼叫
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);
還有兩個值得注意的地方:
- 節點的深度_depth 也是在這個時候計算的,深度對重新整理很重要!先記下!
- 每個Element的buildOwner,都來自父類的BuildOwner,這樣可以保證一個ElementTree,只由一個BuildOwner來維護。
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子父間的掛載。
Flutter的重新整理流程:Element的複用
通過前面的瞭解,我們知道了雖然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:
重新整理第1步:Element標記自身為dirty,並通知buildOwner處理
當對方想改變下方Text Widget的文案時,會在StatefulWidget內部呼叫setState((){_title="ttt"})
,之後該widget對應的element將自身標記為dirty
狀態,並呼叫owner.scheduleBuildFor(this);
通知buildOwner進行處理。
後續StatefulWidget的build方法一定會被執行,執行後,會建立新的子Widget出來,原來的子Widget便被拋棄掉了(將軍:“好好的一個物件就這麼被浪費了,哎……現在的年輕人~”)。
原來的子Widget肯定是沒救了,但他們的Element大概率還是有救的。
重新整理第2步:buildOwner將element新增到集合_dirtyElements中,並通知ui.window安排新的一幀
buildOwner會將所有dirty的Element新增到_dirtyElements當中,等待下一幀繪製時集中處理。
還會呼叫ui.window.scheduleFrame();
通知底層渲染引擎安排新的一幀處理。
重新整理第3步:底層引擎最終回撥到Dart層,並執行buildOwner的buildScope方法
這裡很重要,所以用程式碼講更清晰!
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();
...
}
}
複製程式碼
3.1步:按照Element的深度從小到大,對_dirtyElements進行排序
為啥要排序呢?因為父Widget的build方法必然會觸發子Widget的build,如果先build了子Widget,後面再build父Widget時,子Widget又要被build一次。所以這樣排序之後,可以避免子Widget的重複build。
3.2步:遍歷執行_dirtyElements當中element的rebuild方法
值得一提的是,遍歷執行的過程中,也有可能會有新的element被加入到_dirtyElements集合中,此時會根據dirtyElements集合的長度判斷是否有新的元素進來了,如果有,就重新排序。
element的rebuild方法最終會呼叫performRebuild()
,而performRebuild()
不同的Element有不同的實現
3.3步:遍歷結束之後,清空dirtyElements集合
重新整理第4步:執行performRebuild()
performRebuild()不同的Element有不同的實現,我們暫時只看最常用的兩個Element:
- ComponentElement,是StatefulWidget和StatelessElement的父類
- RenderObjectElement, 是有渲染功能的Element的父類
ComponentElement的performRebuild()
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有四種可能的情況:
- 1.如果剛build出來的widget等於null,說明這個控制元件被刪除了,child Element可以被刪除了。
-
2.如果child的widget和新build出來的一樣(Widget複用了),就看下位置一樣不,不一樣就更新下,一樣就直接return了。Element還是舊的Element
-
3.看下Widget是否可以update,
Widget.canUpdate
的邏輯是判斷key值和執行時型別是否相等。如果滿足條件的話,就更新,並返回。
中間商的差價哪來的呢?只要新build出來的Widget和上一次的型別和Key值相同,Element就會被複用!由此也就保證了雖然Widget在不停的新建,但只要不發生大的變化,那Element是相對穩定的,也就保證了RenderObject是穩定的!
- 4.如果上述三個條件都沒有滿足的話,就呼叫
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。
RenderObjectElement的performRebuild()
@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的相對不變了吧~
Flutter的重新整理流程:PipelineOwner對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. 一些語義場景需要
}
複製程式碼
Flutter的重新整理流程:清理
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內部原理越來越熟悉,這種問題很快就會被解決的。