Flutter的渲染——三棵樹(shù)

一羡棵、引子

Flutter 中有三棵樹(shù):Widget 樹(shù)屯曹,Element 樹(shù)和 RenderObject 樹(shù)。當(dāng)應(yīng)用啟動(dòng)時(shí) Flutter 會(huì)遍歷并創(chuàng)建所有的 Widget 形成 Widget Tree,同時(shí)與 Widget Tree 相對(duì)應(yīng)鉴逞,通過(guò)調(diào)用 Widget 上的 createElement() 方法創(chuàng)建每個(gè) Element 對(duì)象,形成 Element Tree司训。最后調(diào)用 Element 的 createRenderObject() 方法創(chuàng)建每個(gè)渲染對(duì)象构捡,形成一個(gè) Render Tree。 Element就是Widget在UI樹(shù)具體位置的一個(gè)實(shí)例化對(duì)象壳猜,大多數(shù)Element只有唯一的renderObject勾徽,但還有一些Element會(huì)有多個(gè)子節(jié)點(diǎn),如繼承自RenderObjectElement的一些類(lèi)统扳,比如MultiChildRenderObjectElement喘帚。最終所有Element的RenderObject構(gòu)成一棵樹(shù),我們稱(chēng)之為”Render Tree“即”渲染樹(shù)“咒钟〈涤桑總結(jié)一下,我們可以認(rèn)為Flutter的UI系統(tǒng)包含三棵樹(shù):Widget樹(shù)朱嘴、Element樹(shù)倾鲫、渲染樹(shù)粗合。他們的依賴(lài)關(guān)系是:根據(jù)Widget樹(shù)生成Element樹(shù),再依賴(lài)于Element樹(shù)生成RenderObject 樹(shù)乌昔,如下圖:

這種樹(shù)形結(jié)構(gòu)類(lèi)似于HTML中的DOM樹(shù)隙疚,如默認(rèn)的計(jì)數(shù)器應(yīng)用的結(jié)構(gòu)如下圖:

在 flutter 中,Container磕道、Text 等組件都屬于 Widget供屉,所以這課樹(shù)就是 Widget 樹(shù),也可以叫做控件樹(shù)捅厂,它就表示了我們?cè)?dart 代碼中所寫(xiě)的控件的結(jié)構(gòu)贯卦。Element 就是 Widget 的另一種抽象。我們?cè)诖a中使用的像 Container焙贷、Text 等這類(lèi)組件和其屬性只不過(guò)是我們想要構(gòu)建的組件的配置信息撵割,當(dāng)我們第一次調(diào)用 build()`方法想要在屏幕上顯示這些組件時(shí),F(xiàn)lutter 會(huì)根據(jù)這些信息生成該 Widget 控件對(duì)應(yīng)的 Element辙芍,同樣地啡彬,Element 也會(huì)被放到相應(yīng)的 Element 樹(shù)當(dāng)中。RenderObject 在 Flutter 當(dāng)中做組件布局渲染的工作故硅,其為了組件間的渲染搭配及布局約束也有對(duì)應(yīng)的 RenderObject 樹(shù)庶灿,我們也稱(chēng)之為渲染樹(shù)。

二吃衅、Widget 樹(shù)

Widget 是 Flutter 的核心部分往踢,是用戶(hù)界面的不可變描述信息。Widget的功能是“描述一個(gè)UI元素的配置數(shù)據(jù)”徘层,它就是說(shuō)峻呕,Widget其實(shí)并不是表示最終繪制在設(shè)備屏幕上的顯示元素,而它只是描述顯示元素的一個(gè)配置數(shù)據(jù)趣效。正如 Flutter 的口號(hào) Everything’s a widget, 用 Flutter 開(kāi)發(fā)應(yīng)用就是在寫(xiě) Widget 瘦癌。Widget 的 canUpdate 方法通過(guò)比較新部件和舊部件的 runtimeType 和 key 屬性是否相同來(lái)決定更新部件對(duì)應(yīng)的 Element。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
  }
@protected
  Element createElement();

三跷敬、Element 樹(shù)

實(shí)際上讯私,F(xiàn)lutter中真正代表屏幕上顯示元素的類(lèi)是Element,Widget只是UI元素的一個(gè)配置數(shù)據(jù)西傀,并且一個(gè)Widget可以對(duì)應(yīng)多個(gè)Element斤寇。Element就是Widget在UI樹(shù)具體位置的一個(gè)實(shí)例化對(duì)象,大多數(shù)Element只有唯一的renderObject池凄,但還有一些Element會(huì)有多個(gè)子節(jié)點(diǎn)抡驼,如繼承自RenderObjectElement的一些類(lèi),比如MultiChildRenderObjectElement肿仑。Widget 是不可變致盟,它的改變就意味著要重建碎税,而其重建也非常頻繁,如果我們將更多的任務(wù)都交給它將會(huì)對(duì)性能造成很大的損傷馏锡,因此我們把 Widget 組件當(dāng)作一個(gè)虛擬的組件樹(shù)雷蹂,而真正被渲染在屏幕上的其實(shí)是 Elememt 這棵樹(shù),它持有其對(duì)應(yīng) Widget 的引用杯道,如果他對(duì)應(yīng)的 Widget 發(fā)生改變匪煌,它就會(huì)被標(biāo)記為 dirty Element,于是下一次更新視圖時(shí)根據(jù)這個(gè)狀態(tài)只更新被修改的內(nèi)容党巾,從而達(dá)到提升性能的效果萎庭。

Element的生命周期如下:

  1. Framework 調(diào)用Widget.createElement 創(chuàng)建一個(gè)Element實(shí)例,記為element

  2. Framework 調(diào)用 element.mount(parentElement,newSlot) 齿拂,mount方法中首先調(diào)用element所對(duì)應(yīng)Widget的createRenderObject方法創(chuàng)建與element相關(guān)聯(lián)的RenderObject對(duì)象驳规,然后調(diào)用element.attachRenderObject方法將element.renderObject添加到渲染樹(shù)中插槽指定的位置(這一步不是必須的,一般發(fā)生在Element樹(shù)結(jié)構(gòu)發(fā)生變化時(shí)才需要重新attach)署海。插入到渲染樹(shù)后的element就處于“active”狀態(tài)吗购,處于“active”狀態(tài)后就可以顯示在屏幕上了(可以隱藏)。

  3. 當(dāng)有父Widget的配置數(shù)據(jù)改變時(shí)砸狞,同時(shí)其State.build返回的Widget結(jié)構(gòu)與之前不同捻勉,此時(shí)就需要重新構(gòu)建對(duì)應(yīng)的Element樹(shù)。為了進(jìn)行Element復(fù)用刀森,在Element重新構(gòu)建前會(huì)先嘗試是否可以復(fù)用舊樹(shù)上相同位置的element踱启,element節(jié)點(diǎn)在更新前都會(huì)調(diào)用其對(duì)應(yīng)Widget的canUpdate方法,如果返回true,則復(fù)用舊Element,舊的Element會(huì)使用新Widget配置數(shù)據(jù)更新误续,反之則會(huì)創(chuàng)建一個(gè)新的Element感局。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時(shí)相等,如果同時(shí)相等就返回true琐凭,否則就會(huì)返回false芽隆。根據(jù)這個(gè)原理,當(dāng)我們需要強(qiáng)制更新一個(gè)Widget時(shí)统屈,可以通過(guò)指定不同的Key來(lái)避免復(fù)用胚吁。

  4. 當(dāng)有祖先Element決定要移除element 時(shí)(如Widget樹(shù)結(jié)構(gòu)發(fā)生了變化,導(dǎo)致element對(duì)應(yīng)的Widget被移除)愁憔,這時(shí)該祖先Element就會(huì)調(diào)用deactivateChild 方法來(lái)移除它腕扶,移除后element.renderObject也會(huì)被從渲染樹(shù)中移除,然后Framework會(huì)調(diào)用element.deactivate 方法吨掌,這時(shí)element狀態(tài)變?yōu)椤癷nactive”狀態(tài)半抱。

  5. “inactive”態(tài)的element將不會(huì)再顯示到屏幕脓恕。為了避免在一次動(dòng)畫(huà)執(zhí)行過(guò)程中反復(fù)創(chuàng)建、移除某個(gè)特定element窿侈,“inactive”態(tài)的element在當(dāng)前動(dòng)畫(huà)最后一幀結(jié)束前都會(huì)保留炼幔,如果在動(dòng)畫(huà)執(zhí)行結(jié)束后它還未能重新變成“active”狀態(tài),F(xiàn)ramework就會(huì)調(diào)用其unmount方法將其徹底移除史简,這時(shí)element的狀態(tài)為defunct,它將永遠(yuǎn)不會(huì)再被插入到樹(shù)中乃秀。

  6. 如果element要重新插入到Element樹(shù)的其它位置,如element或element的祖先擁有一個(gè)GlobalKey(用于全局復(fù)用元素)圆兵,那么Framework會(huì)先將element從現(xiàn)有位置移除跺讯,然后再調(diào)用其activate方法,并將其renderObject重新attach到渲染樹(shù)殉农。

Element樹(shù)的生命周期如圖:

四刀脏、RenderObject 樹(shù)(渲染樹(shù))

4.1 介紹

渲染樹(shù)的任務(wù)就是做組件的具體的布局渲染工作,渲染樹(shù)上每個(gè)節(jié)點(diǎn)都是一個(gè)繼承自 RenderObject 類(lèi)的對(duì)象统抬,其由 Element 中的 renderObject 或 RenderObjectWidget 中的 createRenderObject 方法生成火本,該對(duì)象內(nèi)部提供多個(gè)屬性及方法來(lái)幫助框架層中的組件如何布局渲染。RenderObject 用于應(yīng)用界面的布局和繪制聪建,保存了元素的大小钙畔,布局等信息,實(shí)例化一個(gè) RenderObject 是非常耗能的金麸。 RenderObject 主要屬性和方法如下:

  • constraints 對(duì)象擎析,從其父級(jí)傳遞給它的約束。

  • parentData 對(duì)象挥下,其父對(duì)象附加有用的信息揍魂。

  • performLayout 方法,計(jì)算此渲染對(duì)象的布局棚瘟。

  • paint 方法现斋,繪制該組件及其子組件。

4.2 布局過(guò)程

Flutter 中的控件在屏幕上繪制渲染之前需要先進(jìn)行布局(Layout)操作偎蘸。其具體可分為兩個(gè)線(xiàn)性過(guò)程:

  1. 從頂部向下傳遞約束庄蹋。

這一過(guò)程用于傳遞布局約束。父節(jié)點(diǎn)給每個(gè)子節(jié)點(diǎn)傳遞約束迷雪,這些約束是每個(gè)子節(jié)點(diǎn)在布局階段必須要遵守的規(guī)則限书。常見(jiàn)的約束包括規(guī)定子節(jié)點(diǎn)最大最小寬度或者子節(jié)點(diǎn)最大最小的高度。這種約束會(huì)向下延伸章咧,子組件也會(huì)產(chǎn)生約束傳遞給自己的孩子倦西,一直到葉子結(jié)點(diǎn)。

  1. 從底部向上傳遞布局信息赁严。

這一過(guò)程用來(lái)傳遞具體的布局信息扰柠。子節(jié)點(diǎn)接受到來(lái)自父節(jié)點(diǎn)的約束后粉铐,會(huì)依據(jù)它產(chǎn)生自己具體的布局信息,如父節(jié)點(diǎn)規(guī)定我的最小寬度是 500 的單位像素耻矮,子節(jié)點(diǎn)按照這個(gè)規(guī)則可能定義自己的寬度為 500 個(gè)像素秦躯,或者大于 500 像素的任何一個(gè)值。這樣裆装,確定好自己的布局信息之后踱承,將這些信息告訴父節(jié)點(diǎn)。父節(jié)點(diǎn)也會(huì)繼續(xù)此操作向上傳遞一直到最頂部哨免。 其過(guò)程可用下圖表示:

Flutter 中有兩種主要的布局協(xié)議:Box 盒子協(xié)議和 Sliver 滑動(dòng)協(xié)議茎活。 在RenderBox 中,有個(gè)size屬性用來(lái)保存控件的寬和高琢唾。RenderBox的layout是通過(guò)在組件樹(shù)中從上往下傳遞BoxConstraints對(duì)象的實(shí)現(xiàn)的载荔。BoxConstraints對(duì)象可以限制子節(jié)點(diǎn)的最大和最小寬高,子節(jié)點(diǎn)必須遵守父節(jié)點(diǎn)給定的限制條件采桃。在布局階段懒熙,父節(jié)點(diǎn)會(huì)調(diào)用子節(jié)點(diǎn)的layout()方法,layout方法需要傳入兩個(gè)參數(shù)普办,第一個(gè)為constraints工扎,即 父節(jié)點(diǎn)對(duì)子節(jié)點(diǎn)大小的限制,該值根據(jù)父節(jié)點(diǎn)的布局邏輯確定衔蹲。另外一個(gè)參數(shù)是 parentUsesSize肢娘,該值用于確定 relayoutBoundary,該參數(shù)表示子節(jié)點(diǎn)布局變化是否影響父節(jié)點(diǎn)舆驶,如果為true橱健,當(dāng)子節(jié)點(diǎn)布局發(fā)生變化時(shí)父節(jié)點(diǎn)都會(huì)標(biāo)記為需要重新布局,如果為false沙廉,則子節(jié)點(diǎn)布局發(fā)生變化后不會(huì)影響父節(jié)點(diǎn)拘荡。

4.3 繪制過(guò)程

RenderObject可以通過(guò)paint()方法來(lái)完成具體繪制邏輯,流程和布局流程相似撬陵,子類(lèi)可以實(shí)現(xiàn)paint()方法來(lái)完成自身的繪制邏輯俱病,paint()簽名如下:

void paint(PaintingContext context, Offset offset) { }

通過(guò)context.canvas可以取到Canvas對(duì)象,接下來(lái)就可以調(diào)用Canvas API來(lái)實(shí)現(xiàn)具體的繪制邏輯袱结。如果節(jié)點(diǎn)有子節(jié)點(diǎn),它除了完成自身繪制邏輯之外途凫,還要通過(guò)paintChild()方法來(lái)調(diào)用子節(jié)點(diǎn)的繪制方法垢夹。如此遞歸完成整個(gè)節(jié)點(diǎn)樹(shù)的繪制,最終調(diào)用棧為: paint() > paintChild() > paint() ... 维费。

五果元、為什么需要三棵樹(shù)促王?

先說(shuō)答案:使用三棵樹(shù)的目的是盡可能復(fù)用 Element。

復(fù)用 Element 對(duì)性能非常重要而晒,因?yàn)?Element 擁有兩份關(guān)鍵數(shù)據(jù):Stateful widget 的狀態(tài)對(duì)象及底層的 RenderObject蝇狼。當(dāng)應(yīng)用的結(jié)構(gòu)很簡(jiǎn)單時(shí),或許體現(xiàn)不出這種優(yōu)勢(shì)倡怎,一旦應(yīng)用復(fù)雜起來(lái)迅耘,構(gòu)成頁(yè)面的元素越來(lái)越多,重新創(chuàng)建 3 棵樹(shù)的代價(jià)是很高的监署,所以需要最小化更新操作颤专。當(dāng) Flutter 能夠復(fù)用 Element 時(shí),用戶(hù)界面的邏輯狀態(tài)信息是不變的钠乏,并且可以重用之前計(jì)算的布局信息栖秕,避免遍歷整棵樹(shù)。

參考:

https://book.flutterchina.club/

https://juejin.cn/post/6844903837858283528

https://zhuanlan.zhihu.com/p/128469011

http://www.reibang.com/p/096a38a24a49

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晓避,一起剝皮案震驚了整個(gè)濱河市簇捍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌俏拱,老刑警劉巖暑塑,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異彰触,居然都是意外死亡梯投,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)况毅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)分蓖,“玉大人,你說(shuō)我怎么就攤上這事尔许∶春祝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵味廊,是天一觀的道長(zhǎng)蒸甜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)余佛,這世上最難降的妖魔是什么柠新? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮辉巡,結(jié)果婚禮上恨憎,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好憔恳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布瓤荔。 她就那樣靜靜地躺著,像睡著了一般钥组。 火紅的嫁衣襯著肌膚如雪输硝。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天程梦,我揣著相機(jī)與錄音点把,去河邊找鬼。 笑死作烟,一個(gè)胖子當(dāng)著我的面吹牛愉粤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拿撩,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼衣厘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了压恒?” 一聲冷哼從身側(cè)響起影暴,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎探赫,沒(méi)想到半個(gè)月后型宙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伦吠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年妆兑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片毛仪。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搁嗓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出箱靴,到底是詐尸還是另有隱情腺逛,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布衡怀,位于F島的核電站棍矛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏抛杨。R本人自食惡果不足惜够委,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怖现。 院中可真熱鬧慨绳,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)恢共。三九已至战秋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讨韭,已是汗流浹背脂信。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留透硝,地道東北人狰闪。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像濒生,于是被迫代替她去往敵國(guó)和親埋泵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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