帝國的紛爭-Flutter-UI繪制解析

前言

為避免傳統(tǒng)的源碼講解方式的枯燥乏味,這一次钮热,我嘗試換一種方式填抬,帶著你以輕松的心態(tài)了解Flutter世界里的UI繪制流程,去探究Widget隧期、Element飒责、RenderObject的秘密。

廢話不多說仆潮,聽故事宏蛉!《帝國的紛爭》

故事

十載干戈,移動端格局漸定鸵闪,壁壘分明檐晕。

北方草原金帳王朝Javascript雖內(nèi)部紛爭不斷,但卻一直窺視中原大陸蚌讼,數(shù)年來襲擾不斷辟灰,如今已奪得小片領(lǐng)土(ReactNative)。民間盛傳:大前端融合之勢已現(xiàn)篡石!

2018年冬芥喇,Android邊境小城Flutter突然宣布立國!并對兩個移動端帝國正式宣戰(zhàn);巳继控!短短幾日械馆,已攻下數(shù)城。

而今天我們要講的故事武通,就發(fā)生在戰(zhàn)火最嚴(yán)重的Android邊陲重鎮(zhèn):View城霹崎。

某日,Android View 城軍事會議:

鎮(zhèn)邊大將軍對手下謀士道:“Flutter 最近對我們發(fā)起了數(shù)次進攻冶忱,已下數(shù)城尾菇,知己不知彼乃軍家大忌!誰能給我說說這個Flutter和我們現(xiàn)在的View到底有什么區(qū)別囚枪?”

下方謀士面面相窺派诬,不得已終于一個謀士站了出來:“我愿意替將軍前去打探一番!”

數(shù)日后链沼,謀士:“臣臥底歸來默赂,探明Flutter與我們View城的主要區(qū)別在于編程范式和視圖邏輯單元不同”

將軍:“先講編程范式如何不同?”

Android/Flutter 編程范式

將軍括勺,我們Android現(xiàn)在視圖開發(fā)是命令式的缆八,我們的每一個View都直接聽從將軍(Developer)的指揮,例如:想要變更界面某個文案朝刊,便要指明具體TextView調(diào)用他的setText方法命令文字發(fā)生變更耀里;

而Flutter的視圖開發(fā)是聲明式的,對方的將軍要做的是維護一套數(shù)據(jù)集,以及設(shè)定好一套布軍計劃(WidgetTree)拾氓,并且為Widget“綁定”數(shù)據(jù)集中的某個數(shù)據(jù)冯挎,根據(jù)這個數(shù)據(jù)來渲染。
例如當(dāng)需要變更文案時咙鞍,便改變數(shù)據(jù)集中的數(shù)據(jù)房官,然后直接觸發(fā)WidgetTree的重新渲染。這樣Flutter的將軍不再需要關(guān)注每一個士兵续滋,大部分的精力都用來維護核心數(shù)據(jù)即可翰守。

如果每一次操作都消耗一點將軍的精力值,又剛好有同一個數(shù)據(jù)“綁定”到了多個View或Widget上疲酌。命令式的編程需要做的事情是 命令N個View發(fā)生變更蜡峰,消耗N點精力值;

聲明式編程需要做的事情是 變更數(shù)據(jù)+觸發(fā)WidgetTree重繪,消耗2點精力值朗恳;對精力的解放湿颅,也是Flutter可以快速招攬到那么多將軍的原因之一。

將軍:”但每次數(shù)據(jù)變更粥诫,都會觸發(fā)WidgetTree的重繪油航,消耗的資源未免也太大了吧,我現(xiàn)在雖然多消耗些精力怀浆,但不會存在大量對象創(chuàng)建的情況“谊囚。

Widget怕享、Element、RenderObject概念

謀士:這也是馬上要講的第二點不同镰踏。因為WidgetTree會大量的重繪函筋,所以Widget必然是廉價的。

Flutter UI有三大元素:Widget奠伪、Element驻呐、RenderObject。對應(yīng)這三者也有三個owner負(fù)責(zé)管理他們芳来,分別是WidgetOwner(將軍&Developer)、BuildOwner猜拾、PipelineOwner即舌。

  • Widget,Widget 并不是真正的士兵挎袜,它只是將軍手中的棋子顽聂,是一些廉價的純對象,持有一些渲染需要的配置信息盯仪,棋子在不斷被替換著紊搪。

  • RenderObject,RenderObject 是真正和我們作戰(zhàn)的士兵全景,在概念上和我們Android的View一樣耀石,渲染引擎會根據(jù)RenderObject來進行真正的繪制,它是相對穩(wěn)定且昂貴的爸黄。

  • Element滞伟,使得不斷變化Widget轉(zhuǎn)變?yōu)橄鄬Ψ€(wěn)定的RenderObject的功臣是Element。

WidgetOwner(Developer) 在不斷改變著布軍計劃炕贵,然后向BuildOwner發(fā)送著一張又一張計劃表(WidgetTree)梆奈,首次的計劃表(WidgetTree)會生成一個與之對應(yīng)的ElementTree,并生成對應(yīng)的RenderObjectTree称开。

后續(xù)BuildOwner每次收到新的計劃表就與上一次的進行對比,在ElementTree上只更新變化的部分,Element變化之后纹冤,與之對應(yīng)的RenderObject也就更新了雄可。

可以看到WidgetTree全部被替換了,但ElementTree和RenderObjectTree只替換了變化的部分脆霎。

差點忘了講 PipelineOwner总处, PipelineOwner類似于Android中的ViewRootImpl,管理著真正需要繪制的View,
最后PipelineOwner會對RenderObjectTree中發(fā)生變化節(jié)點的進行l(wèi)ayout睛蛛、paint鹦马、合成等等操作胧谈,最后交給底層引擎渲染。

將軍:“我大概明白了荸频,看來保證聲明式編程性能穩(wěn)定的核心在于這個Element和BuildOwner菱肖。但我看這里還有兩個問題,RenderObject好像少了一個節(jié)點旭从?你畫圖畫錯了嗎稳强?還有能給我講下他是怎么把Widget和RenderObject鏈接起來,以及發(fā)生變化時和悦,BuildOwner是如何做到元素Diff的嗎退疫?”

Widget、Element鸽素、RenderObject之間的關(guān)系

首先褒繁,每一個Widget家族的老長輩Widget賦予了所有的Widget子類三個關(guān)鍵的能力:保證自身唯一以及定位的Key, 創(chuàng)建Element的 createElement, 和 canUpdate。 canUpdate 的作用后面講馍忽。

Widget子類里還有一批特別優(yōu)秀強壯的棒坏,是在紙面上代表著有渲染能力的RenderObjectWidget,它還有一個創(chuàng)建 RenderObject的 createRenderObject 方法。

從這里你也看出來了遭笋,Widget坝冕、Element、RenderObject的創(chuàng)建關(guān)系并不是線性傳遞的瓦呼,Element和RenderObject都是Widget創(chuàng)建出來的****喂窟,也并不是每一個Widget都有與之對應(yīng)的RenderObjectWidget。這也解釋上面圖中RenderObjectTree看起來和前面的WidgetTree缺少了一些節(jié)點吵血。

Widget谎替、Element、RenderObject 的第一次創(chuàng)建與關(guān)聯(lián)

講第一次創(chuàng)建蹋辅,一定要從第一個被創(chuàng)建出來的士兵說起钱贯。我們都知道Android的ViewTree:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">-PhoneWindow

  • DecorView
  • TitleView
  • ContentView
    </pre>

|

已經(jīng)預(yù)先有這么多View了,相比Android的ViewTree侦另,F(xiàn)lutter的WidgetTree則要簡單的多,只有最底層的root widget褒傅。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">- RenderObjectToWidgetAdapter<RenderBox>

  • MyApp (自定義)
  • MyMaterialApp (自定義)
    </pre>

|

簡單介紹一下RenderObjectToWidgetAdapter弃锐,不要被他的adapter名字迷惑了,RenderObjectToWidgetAdapter其實是一個RenderObjectWidget殿托,他就是第一個優(yōu)秀且強壯的Widget霹菊。

這個時候就不得不搬出代碼來看了,runApp源碼:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
</pre>

|

WidgetsFlutterBinding ”迷信“了一系列的Binding,這些Binding持有了我們上面說的一些owner支竹,比如BuildOwner旋廷,PipelineOwner鸠按,所以隨著WidgetsFlutterBinding的初始化,其他的Binding也被初始化了饶碘,此時Flutter 的國家引擎開始轉(zhuǎn)動了目尖!

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
</pre>

|

我們最需要關(guān)注的是attachRootWidget(app)這個方法,這個方法很神圣,很多的第一次就在這個方法里實現(xiàn)了T恕I(將軍:“很神圣?你是不叛變了豪治?”)洞拨,app 是我們傳入的自定義Widget,內(nèi)部會創(chuàng)建RenderObjectToWidgetAdapter负拟,并將app做為它的child的扣甲。

緊接著又執(zhí)行了attachToRenderTree,這個方法,這個方法也很神圣,創(chuàng)建了第一個Element和RenderObject

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement(); //創(chuàng)建rootElement
element.assignOwner(owner); //綁定BuildOwner
});
owner.buildScope(element, () { //子widget的初始化從這里開始
element.mount(null, null); // 初始化子Widget前齿椅,先執(zhí)行rootElement的mount方法
});
} else {
...
}
return element;
}
</pre>

|

我們解釋一下上面的圖片,Root的創(chuàng)建比較簡單:

  • 1.attachRootWidget(app) 方法創(chuàng)建了Root[Widget](也就是 RenderObjectToWidgetAdapter)
  • 2.緊接著調(diào)用attachToRenderTree方法創(chuàng)建了 Root[Element]
  • 3.Root[Element]嘗試調(diào)用mount方法將自己掛載到父Element上启泣,因為自己就是root了涣脚,所以沒有父Element,掛空了
  • 4.mount的過程中會調(diào)用Widget的createRenderObject,創(chuàng)建了 Root[RenderObject]

它的child寥茫,也就是我們傳入的app是怎么掛載父控件上的呢遣蚀?

  • 5.我們將app作為Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了為root[Widget]的child[Widget]
  • 6.調(diào)用owner.buildScope纱耻,開始執(zhí)行子Tree的創(chuàng)建以及掛載芭梯,敲黑板!E玖喘!這中間的流程和WidgetTree的刷新流程是一模一樣的,詳細(xì)流程我們后面講蘑志!
  • 7.調(diào)用createElement方法創(chuàng)建出Child[Element]
  • 8.調(diào)用Element的mount方法,將自己掛載到Root[Element]上累奈,形成一棵樹
  • 9.掛載的同時,調(diào)用widget.createRenderObject,創(chuàng)建Child[RenderObject]
  • 10.創(chuàng)建完成后急但,調(diào)用attachRenderObject,完成和Root[RenderObject]的鏈接

就這樣澎媒,WidgetTree、ElementTree波桩、RenderObject創(chuàng)建完成戒努,并有各自的鏈接關(guān)系。

將軍:“我想看一下這個mountattachRenderObject的過程,看下到底是怎么掛上去的”

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">abstract class Element:

void mount(Element parent, dynamic newSlot) {
_parent = parent; //持有父Element的引用
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;//當(dāng)前節(jié)點的深度
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner; //每個Element的buildOwner镐躲,都來自父類的BuildOwner
...
}
</pre>

|

我們先看一下Element的掛載储玫,就是讓_parent持有父Element的引用侍筛,很簡單對不對~

因為RootElement 是沒有父Element的,所以參數(shù)傳了null:element.mount(null, null);

還有兩個值得注意的地方:

  • 節(jié)點的深度_depth 也是在這個時候計算的缘缚,深度對刷新很重要勾笆!先記下!
  • 每個Element的buildOwner桥滨,都來自父類的BuildOwner窝爪,這樣可以保證一個ElementTree,只由一個BuildOwner來維護。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">abstract class RenderObjectElement:

@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}
</pre>

|

RenderObject與父RenderObject的掛載稍微復(fù)雜了點齐媒。通過代碼我們可以看到需要先查詢一下自己的AncestorRenderObject,這是為什么呢蒲每?

還記得之前我們講過,每一個Widget都有一個對應(yīng)的Element喻括,但Element不一定會有對應(yīng)的RenderObject邀杏。所以你的父Element并不一有RenderObject,這個時候就需要向上查找唬血。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">RenderObjectElement _findAncestorRenderObjectElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement)
ancestor = ancestor._parent;
return ancestor;
}
</pre>

|

通過代碼我們也可以看到望蜡,find方法在向上遍歷Element,直到找到RenderObjectElement拷恨,RenderObjectElement肯定是有對應(yīng)的RenderObject了脖律,這個時候在進行RenderObject子父間的掛載。

Flutter的刷新流程:Element的復(fù)用

通過前面的了解腕侄,我們知道了雖然createRenderObject方法的實現(xiàn)是在Widget當(dāng)中小泉,但持有RenderObject引用的卻是Element。忘記啦冕杠?那我們再看看代碼:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">abstract class RenderObjectElement extends Element {
...

@override
RenderObjectWidget get widget => super.widget;

@override
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;
}
</pre>

|

Element同時持有兩者微姊,可以說,element就是Widget 和 RenderObject的中間商分预,它也確實在賺差價……

這個時候Root Widget兢交,Root Element,Root RenderObject都已經(jīng)創(chuàng)建完成并且三者鏈接成功笼痹。將軍您看還有什么問題嗎魁淳?

將軍:“Flutter內(nèi)部還有中間商賺差價呢?真腐斢氤界逛!誒你說說他是怎么賺差價的啊纺座?說不定我也可以學(xué)學(xué)~”

Flutter如果想要刷新界面息拜,需要在StatefulWidget里調(diào)用setState()方法,setState()干啥了呢?

@protected
void setState(VoidCallback fn) {

_element.markNeedsBuild();
}

將軍我們實際演練一下少欺,假設(shè)Flutter派出了這么一個WidgetTree:

刷新第1步:Element標(biāo)記自身為dirty喳瓣,并通知buildOwner處理

當(dāng)對方想改變下方Text Widget的文案時,會在MyStatefulWidget內(nèi)部調(diào)用setState((){_title="ttt"}) 赞别,之后該widget對應(yīng)的element將自身標(biāo)記為dirty狀態(tài),并調(diào)用owner.scheduleBuildFor(this);通知buildOwner進行處理畏陕。

后續(xù)MyStatefulWidget的build方法一定會被執(zhí)行,執(zhí)行后仿滔,會創(chuàng)建新的子Widget出來惠毁,原來的子Widget便被拋棄掉了(將軍:“好好的一個對象就這么被浪費了,哎……現(xiàn)在的年輕人~”)崎页。

原來的子Widget肯定是沒救了鞠绰,但他們的Element大概率還是有救的。

刷新第2步:buildOwner將element添加到集合_dirtyElements中飒焦,并通知ui.window安排新的一幀

buildOwner會將所有dirty的Element添加到_dirtyElements當(dāng)中蜈膨,等待下一幀繪制時集中處理。

還會調(diào)用ui.window.scheduleFrame();通知底層渲染引擎安排新的一幀處理牺荠。

刷新第3步:底層引擎最終回到Dart層翁巍,并執(zhí)行buildOwner的buildScope方法

這里很重要,所以用代碼講更清晰休雌!

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void buildScope(Element context, [VoidCallback callback]){
...
}
</pre>

|

buildScope!! 還記的嗎曙咽?前面講Root創(chuàng)建的時候,我們就看到了Child的初次創(chuàng)建也是調(diào)用的buildScope方法!Tree的首幀創(chuàng)建和刷新是一套邏輯挑辆!

buildScope需要傳入一個Element的參數(shù),這個方法通過字面意思我們應(yīng)該能理解孝情,大概就是對這個Element以下(包含)的范圍rebuild鱼蝉。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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();
...
}
}
</pre>

|

3.1步:按照Element的深度從小到大,對_dirtyElements進行排序

為啥要排序呢箫荡?因為父Widget的build方法必然會觸發(fā)子Widget的build魁亦,如果先build了子Widget,后面再build父Widget時羔挡,子Widget又要被build一次洁奈。所以這樣排序之后,可以避免子Widget的重復(fù)build绞灼。

3.2步:遍歷執(zhí)行_dirtyElements當(dāng)中element的rebuild方法

值得一提的是利术,遍歷執(zhí)行的過程中,也有可能會有新的element被加入到_dirtyElements集合中低矮,此時會根據(jù)dirtyElements集合的長度判斷是否有新的元素進來了印叁,如果有,就重新排序。

element的rebuild方法最終會調(diào)用performRebuild(),而performRebuild()不同的Element有不同的實現(xiàn)

3.3步:遍歷結(jié)束之后轮蜕,清空dirtyElements集合

刷新第4步:執(zhí)行performRebuild()

performRebuild()不同的Element有不同的實現(xiàn)昨悼,我們暫時只看最常用的兩個Element:

  • ComponentElement,是StatefulWidget和StatelessElement的父類
  • RenderObjectElement跃洛, 是有渲染功能的Element的父類
ComponentElement的performRebuild()

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void performRebuild() {
Widget built;
try {
built = build();
}
...
try {
_child = updateChild(_child, built, slot);
}
...
}
</pre>

|

執(zhí)行element的build();率触,以StatefulElement的build方法為例:Widget build() => state.build(this);。 就是執(zhí)行了我們復(fù)寫的StatefulWidget的state的build方法啦~

執(zhí)行build方法build出來的是啥呢? 當(dāng)然就是這個StatefulWidget的子Widget了汇竭。重點來了葱蝗!敲黑板!:妗(將軍:“又給我敲黑板垒玲??”)Element就是在這個地方賺差價的找颓!

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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);
}
</pre>

|

參數(shù)child 是上一次Element掛載的child Element, newWidget 是剛剛build出來的合愈。updateChild有四種可能的情況:

  • 1.如果剛build出來的widget等于null,說明這個控件被刪除了击狮,child Element可以被刪除了佛析。

  • 2.如果child的widget和新build出來的一樣(Widget復(fù)用了),就看下位置一樣不彪蓬,不一樣就更新下寸莫,一樣就直接return了。Element還是舊的Element

  • 3.看下Widget是否可以update档冬,Widget.canUpdate的邏輯是判斷key值和運行時類型是否相等膘茎。如果滿足條件的話,就更新酷誓,并返回披坏。

中間商的差價哪來的呢?只要新build出來的Widget和上一次的類型和Key值相同盐数,Element就會被復(fù)用棒拂!由此也就保證了雖然Widget在不停的新建,但只要不發(fā)生大的變化玫氢,那Element是相對穩(wěn)定的帚屉,也就保證了RenderObject是穩(wěn)定的!

  • 4.如果上述三個條件都沒有滿足的話漾峡,就調(diào)用 inflateWidget() 創(chuàng)建新的Element

這里再看下inflateWidget()方法:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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;
}
</pre>

|

首先會嘗試通過GlobalKey去查找可復(fù)用的Element攻旦,復(fù)用失敗就調(diào)用Widget的方法創(chuàng)建新的Element,然后調(diào)用mount方法生逸,將自己掛載到父Element上去敬特,mount之前我們也講過掰邢,會在這個方法里創(chuàng)建新的RenderObject。

RenderObjectElement的performRebuild()

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">@override
void performRebuild() {
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
</pre>

|

與ComponentElement的不同之處在于伟阔,沒有去build,而是調(diào)用了updateRenderObject方法更新RenderObject皱炉。

不同Widget也有不同的updateRenderObject實現(xiàn)灾部,我們看一下最常用的RichText悯搔,也就是Text。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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);
}
</pre>

|

一些看起來比較熟悉的賦值操作,像不像Android的view呀? 要不怎么說RenderObject實際相當(dāng)于Android里的View呢臭墨。

到這里你基本就明白了Element是如何在中間應(yīng)對Widget的多變赔嚎,保障RenderObject的相對不變了吧~

Flutter的刷新流程:PipelineOwner對RenderObject的管理

在底層引擎最終回到Dart層,最終會執(zhí)行WidgetsBinding 的drawFrame ()

WidgetsBinding

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void drawFrame() {
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
}
...
}
</pre>

|

buildOwner.buildScope(renderViewElement);就是我們上面講過的胧弛。

下面看一下super.drawFrame(); 主要是PipelineOwner對RenderObject的管理,我們簡單介紹尤误,詳細(xì)的放在下期介紹。

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); //布局需要被布局的RenderObject
pipelineOwner.flushCompositingBits(); // 判斷l(xiāng)ayer是否變化
pipelineOwner.flushPaint(); //繪制需要被繪制的RenderObject
renderView.compositeFrame(); // this sends the bits to the GPU 將畫好的layer傳給engine繪制
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. 一些語義場景需要
}
</pre>

|

Flutter的刷新流程:清理

drawFrame方法在最后執(zhí)行了buildOwner.finalizeTree();

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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();
}
}
</pre>

|

在做最后的清理工作结缚。

將軍:“_inactiveElements”又是個啥损晤?之前咋沒見過?

還記的前面講Element賺差價的updateChild方法嗎红竭?所有沒用的element都調(diào)用了deactivateChild方法進行回收:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
2
3
4
5
</pre>

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner._inactiveElements.add(child); // this eventually calls child.deactivate()
}
</pre>

|

也就在這里將被廢棄的element添加到了_inactiveElements當(dāng)中尤勋。

另外在廢棄element之后,調(diào)用inflateWidget創(chuàng)建新的element時德崭,還調(diào)用了_retakeInactiveElement嘗試通過GlobalKey復(fù)用element斥黑,此時的復(fù)用池也是在_inactiveElements當(dāng)中。

從這里也能了解到眉厨,如果你沒有在一幀里通過GlobeKey完成Element的復(fù)用锌奴,_inactiveElements在最后將被清空,就沒辦法在復(fù)用了憾股。

結(jié)尾

將軍鹿蜀,現(xiàn)在您對Flutter的繪制流程有了初步的了解了嗎?

將軍:“有些了解了服球,但你講了這么多茴恰,對比起來我們Android,聽起來Flutter這一套繪制流程沒啥缺點斩熊? ”

當(dāng)然有了往枣,我們現(xiàn)在也只了解了Flutter的冰山一角,很多東西還沒有發(fā)現(xiàn)粉渠。

但就只說動態(tài)向ViewTree中插入組件這一條分冈,F(xiàn)lutter就沒有我們靈活。比如Toast霸株,現(xiàn)在的Toast組件都只能依賴methodChannel去調(diào)用我們原生的Toast雕沉。而Flutter提供的Scaffold組件,也是通過預(yù)先埋好的組件坑位去件,才實現(xiàn)了Material風(fēng)格的Toast坡椒。

因為Flutter是聲明式的扰路,想要在運行中隨時向WidgetTree插入一個Widget,目前還沒有成熟接口倔叼。

但相信隨著Flutter開發(fā)者對Flutter內(nèi)部原理越來越熟悉汗唱,這種問題很快就會被解決的。

<footer class="post-footer" style="display: block; color: rgb(85, 85, 85); font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

#Flutter

</footer>
小編有準(zhǔn)備一些安卓高級技能的學(xué)習(xí)資料缀雳。進群領(lǐng)取
QQ群:4112676

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渡嚣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肥印,更是在濱河造成了極大的恐慌识椰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件深碱,死亡現(xiàn)場離奇詭異腹鹉,居然都是意外死亡,警方通過查閱死者的電腦和手機敷硅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門功咒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绞蹦,你說我怎么就攤上這事力奋。” “怎么了幽七?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵景殷,是天一觀的道長。 經(jīng)常有香客問我澡屡,道長猿挚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任驶鹉,我火速辦了婚禮绩蜻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘室埋。我一直安慰自己丸相,他們只是感情好浇雹,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布还最。 她就那樣靜靜地躺著缓艳,像睡著了一般机隙。 火紅的嫁衣襯著肌膚如雪懈息。 梳的紋絲不亂的頭發(fā)上俺陋,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天国拇,我揣著相機與錄音疹尾,去河邊找鬼上忍。 笑死骤肛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窍蓝。 我是一名探鬼主播腋颠,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吓笙!你這毒婦竟也來了淑玫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤面睛,失蹤者是張志新(化名)和其女友劉穎絮蒿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叁鉴,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡土涝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了幌墓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片但壮。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖常侣,靈堂內(nèi)的尸體忽然破棺而出蜡饵,到底是詐尸還是另有隱情,我是刑警寧澤胳施,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布溯祸,位于F島的核電站,受9級特大地震影響巾乳,放射性物質(zhì)發(fā)生泄漏您没。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一胆绊、第九天 我趴在偏房一處隱蔽的房頂上張望氨鹏。 院中可真熱鬧,春花似錦压状、人聲如沸仆抵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镣丑。三九已至,卻和暖如春娱两,著一層夾襖步出監(jiān)牢的瞬間莺匠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工十兢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趣竣,地道東北人摇庙。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像遥缕,于是被迫代替她去往敵國和親卫袒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容