Flutter的渲染——三棵樹

一、引子

Flutter 中有三棵樹:Widget 樹,Element 樹和 RenderObject 樹。當應用啟動時 Flutter 會遍歷并創(chuàng)建所有的 Widget 形成 Widget Tree,同時與 Widget Tree 相對應,通過調(diào)用 Widget 上的 createElement() 方法創(chuàng)建每個 Element 對象,形成 Element Tree。最后調(diào)用 Element 的 createRenderObject() 方法創(chuàng)建每個渲染對象,形成一個 Render Tree。 Element就是Widget在UI樹具體位置的一個實例化對象,大多數(shù)Element只有唯一的renderObject,但還有一些Element會有多個子節(jié)點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有Element的RenderObject構(gòu)成一棵樹,我們稱之為”Render Tree“即”渲染樹“??偨Y(jié)一下,我們可以認為Flutter的UI系統(tǒng)包含三棵樹:Widget樹、Element樹、渲染樹。他們的依賴關(guān)系是:根據(jù)Widget樹生成Element樹,再依賴于Element樹生成RenderObject 樹,如下圖:

這種樹形結(jié)構(gòu)類似于HTML中的DOM樹,如默認的計數(shù)器應用的結(jié)構(gòu)如下圖:

在 flutter 中,Container、Text 等組件都屬于 Widget,所以這課樹就是 Widget 樹,也可以叫做控件樹,它就表示了我們在 dart 代碼中所寫的控件的結(jié)構(gòu)。Element 就是 Widget 的另一種抽象。我們在代碼中使用的像 Container、Text 等這類組件和其屬性只不過是我們想要構(gòu)建的組件的配置信息,當我們第一次調(diào)用 build()`方法想要在屏幕上顯示這些組件時,F(xiàn)lutter 會根據(jù)這些信息生成該 Widget 控件對應的 Element,同樣地,Element 也會被放到相應的 Element 樹當中。RenderObject 在 Flutter 當中做組件布局渲染的工作,其為了組件間的渲染搭配及布局約束也有對應的 RenderObject 樹,我們也稱之為渲染樹。

二、Widget 樹

Widget 是 Flutter 的核心部分,是用戶界面的不可變描述信息。Widget的功能是“描述一個UI元素的配置數(shù)據(jù)”,它就是說,Widget其實并不是表示最終繪制在設備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數(shù)據(jù)。正如 Flutter 的口號 Everything’s a widget, 用 Flutter 開發(fā)應用就是在寫 Widget 。Widget 的 canUpdate 方法通過比較新部件和舊部件的 runtimeType 和 key 屬性是否相同來決定更新部件對應的 Element。

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

三、Element 樹

實際上,F(xiàn)lutter中真正代表屏幕上顯示元素的類是Element,Widget只是UI元素的一個配置數(shù)據(jù),并且一個Widget可以對應多個Element。Element就是Widget在UI樹具體位置的一個實例化對象,大多數(shù)Element只有唯一的renderObject,但還有一些Element會有多個子節(jié)點,如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。Widget 是不可變,它的改變就意味著要重建,而其重建也非常頻繁,如果我們將更多的任務都交給它將會對性能造成很大的損傷,因此我們把 Widget 組件當作一個虛擬的組件樹,而真正被渲染在屏幕上的其實是 Elememt 這棵樹,它持有其對應 Widget 的引用,如果他對應的 Widget 發(fā)生改變,它就會被標記為 dirty Element,于是下一次更新視圖時根據(jù)這個狀態(tài)只更新被修改的內(nèi)容,從而達到提升性能的效果。

Element的生命周期如下:

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

  2. Framework 調(diào)用 element.mount(parentElement,newSlot) ,mount方法中首先調(diào)用element所對應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)后就可以顯示在屏幕上了(可以隱藏)。

  3. 當有父Widget的配置數(shù)據(jù)改變時,同時其State.build返回的Widget結(jié)構(gòu)與之前不同,此時就需要重新構(gòu)建對應的Element樹。為了進行Element復用,在Element重新構(gòu)建前會先嘗試是否可以復用舊樹上相同位置的element,element節(jié)點在更新前都會調(diào)用其對應Widget的canUpdate方法,如果返回true,則復用舊Element,舊的Element會使用新Widget配置數(shù)據(jù)更新,反之則會創(chuàng)建一個新的Element。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時相等,如果同時相等就返回true,否則就會返回false。根據(jù)這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。

  4. 當有祖先Element決定要移除element 時(如Widget樹結(jié)構(gòu)發(fā)生了變化,導致element對應的Widget被移除),這時該祖先Element就會調(diào)用deactivateChild 方法來移除它,移除后element.renderObject也會被從渲染樹中移除,然后Framework會調(diào)用element.deactivate 方法,這時element狀態(tài)變?yōu)椤癷nactive”狀態(tài)。

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

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

Element樹的生命周期如圖:

四、RenderObject 樹(渲染樹)

4.1 介紹

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

  • constraints 對象,從其父級傳遞給它的約束。

  • parentData 對象,其父對象附加有用的信息。

  • performLayout 方法,計算此渲染對象的布局。

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

4.2 布局過程

Flutter 中的控件在屏幕上繪制渲染之前需要先進行布局(Layout)操作。其具體可分為兩個線性過程:

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

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

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

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

Flutter 中有兩種主要的布局協(xié)議:Box 盒子協(xié)議和 Sliver 滑動協(xié)議。 在RenderBox 中,有個size屬性用來保存控件的寬和高。RenderBox的layout是通過在組件樹中從上往下傳遞BoxConstraints對象的實現(xiàn)的。BoxConstraints對象可以限制子節(jié)點的最大和最小寬高,子節(jié)點必須遵守父節(jié)點給定的限制條件。在布局階段,父節(jié)點會調(diào)用子節(jié)點的layout()方法,layout方法需要傳入兩個參數(shù),第一個為constraints,即 父節(jié)點對子節(jié)點大小的限制,該值根據(jù)父節(jié)點的布局邏輯確定。另外一個參數(shù)是 parentUsesSize,該值用于確定 relayoutBoundary,該參數(shù)表示子節(jié)點布局變化是否影響父節(jié)點,如果為true,當子節(jié)點布局發(fā)生變化時父節(jié)點都會標記為需要重新布局,如果為false,則子節(jié)點布局發(fā)生變化后不會影響父節(jié)點。

4.3 繪制過程

RenderObject可以通過paint()方法來完成具體繪制邏輯,流程和布局流程相似,子類可以實現(xiàn)paint()方法來完成自身的繪制邏輯,paint()簽名如下:

void paint(PaintingContext context, Offset offset) { }

通過context.canvas可以取到Canvas對象,接下來就可以調(diào)用Canvas API來實現(xiàn)具體的繪制邏輯。如果節(jié)點有子節(jié)點,它除了完成自身繪制邏輯之外,還要通過paintChild()方法來調(diào)用子節(jié)點的繪制方法。如此遞歸完成整個節(jié)點樹的繪制,最終調(diào)用棧為: paint() > paintChild() > paint() ... 。

五、為什么需要三棵樹?

先說答案:使用三棵樹的目的是盡可能復用 Element。

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

參考:

https://book.flutterchina.club/

https://juejin.cn/post/6844903837858283528

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

http://m.itdecent.cn/p/096a38a24a49

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容