Flutter 渲染原理分析及優(yōu)化

1. Flutter 三棵樹(shù)

Flutter 的自渲染離不開(kāi) Flutter 三棵樹(shù):

  • Widget:描述 UI 渲染的配置信息
  • Element:存放上下文,持有 Widget 和 RenderObject
  • RenderObject:實(shí)際渲染樹(shù)中的對(duì)象

我們?cè)赿ebug斷點(diǎn)某個(gè)Widget的時(shí)候可以得到如下的配置信息:

上圖的 StatelessElement 就是 FirstRoute 這個(gè)Widget 對(duì)應(yīng)的 Element,我們知道 Element 是持有 Widget 和 RenderObject,此處的 _widget 就是此 Element 持有的 Widget。有同學(xué)可能會(huì)問(wèn),怎么沒(méi)有看到此 Element 持有的 RenderObject?因?yàn)?Widget 和 Element 是一一對(duì)應(yīng)的,但是與 RenderObject 不是一一對(duì)應(yīng)的。也即是說(shuō)不是所有的 Element 都有對(duì)應(yīng)的 RenderObject,只有 RenderObjectWidget 相關(guān)的 Widget 對(duì)應(yīng)的 Element 才有與之對(duì)應(yīng)的 RenderObject。上圖中的Widget 是 繼承自 StatelessWidget 的 FirstRoute,StatelessWidget 直接繼承自 Widget,而不是 RenderObjectWidget ,所以不會(huì)轉(zhuǎn)化為 RenderObject。將 FirstRoute 的 _child 展開(kāi)如下圖:

FirstRoute 的 child 是 Center,Center 間接繼承 SingleChildRenderObjectWidget,SingleChildRenderObjectWidget 繼承 RenderObjectWidget,所以 Center 最終會(huì)轉(zhuǎn)化成 RenderObjectWidget 渲染到屏幕上。

再將 FirstRoute 的 _parent 展開(kāi)如下圖:

按照我們寫(xiě)的dart代碼理解,F(xiàn)irstRoute 的 parent 應(yīng)該是 MaterialApp,但觀察上圖我們發(fā)現(xiàn)其實(shí)是 Semantics,這其實(shí)也好理解,MaterialApp 里面封裝了很多層 Widget 去做語(yǔ)義分析、手勢(shì)處理、主題、動(dòng)畫(huà)等操作,經(jīng)過(guò)一系列的準(zhǔn)備工作之后才會(huì)到 MaterialApp。處于好奇,我想看看到底封裝了多少層才到 MaterialApp,于是我就一層一層點(diǎn)開(kāi) _parent(說(shuō)實(shí)話,點(diǎn)到一半我想放棄了??,但我堅(jiān)信我一定能把 MaterialApp 揪出來(lái)??),如下圖:

終于,皇天不負(fù)有心人!點(diǎn)開(kāi)最后一個(gè) StatefulElement 終于找到了 MaterialApp,如下圖:

上面有講到Widget 和 Element 樹(shù)是一一對(duì)應(yīng)的,但是與 RenderObject 不是一一對(duì)應(yīng)的。我們具體來(lái)看一下他們之間的對(duì)應(yīng)關(guān)系:

Widget Element RenderObject
StatelessWidget StatelessElement \
StatefulWidget StatefulElement \
ProxyWidget ProxyElement \
InheritedWidget InheritedElement \
SingleChildRenderObjectWidget SingleChildRenderObjectElement RenderObject
MultiChildRenderObjectWidget MultiChildRenderObjectElement RenderObject
RenderObjectWidget RenderObjectElement RenderObject

2. 三棵樹(shù)轉(zhuǎn)化流程

Flutter 運(yùn)行中的一部分核心邏輯就是在處理這三棵樹(shù)的轉(zhuǎn)化,所有的界面交互和事件處理,最終都反應(yīng)在這對(duì)三棵樹(shù)的操作結(jié)果上。Flutter項(xiàng)目的啟動(dòng)代碼一般是

void main() => runApp(MyApp());

runApp代碼如下:

void runApp(Widget app) { 
    WidgetsFlutterBinding.ensureInitialized()
      ..scheduleAttachRootWidget(app)
      ..scheduleWarmUpFrame();
}

三行代碼代表了Flutter APP 啟動(dòng)的三個(gè)主流程:

  1. binding初始化(ensureInitialized)
  2. 創(chuàng)建根 widget,綁定根節(jié)點(diǎn)(scheduleAttachRootWidget)
  3. 繪制熱身幀(scheduleWarmUpFrame)

這里我們主要看下涉及到三棵樹(shù)之間的轉(zhuǎn)化的主要函數(shù)的作用。

scheduleAttachRootWidget

創(chuàng)建根 widget ,并且從根 widget 向子節(jié)點(diǎn)遞歸創(chuàng)建元素Element,對(duì)子節(jié)點(diǎn)為 RenderObjectWidget 的小部件創(chuàng)建 RenderObject 樹(shù)節(jié)點(diǎn),從而創(chuàng)建出 View 的渲染樹(shù),源碼中使用 Timer.run 事件任務(wù)的方式來(lái)運(yùn)行,目的是避免影響到微任務(wù)的執(zhí)行。

attachRootWidget

將傳入的Widget綁定到一個(gè)根節(jié)點(diǎn)并構(gòu)建三棵樹(shù)。

先是通過(guò)傳入的 rootWidget 及 RenderView 實(shí)例化了一個(gè)RenderObjectToWidgetAdapter對(duì)象,而RenderObjectToWidgetAdapter是繼承自RenderObjectWidget,即創(chuàng)建了Widget樹(shù)的根節(jié)點(diǎn)。

attachToRenderTree

attachToRenderTree 中通過(guò) createElement() 創(chuàng)建了一個(gè)RenderObjectToWidgetElement 實(shí)例作為 element tree 的根節(jié)點(diǎn),并綁定BuildOwner,通過(guò) BuildOwner 構(gòu)建需要構(gòu)建的 element。這里會(huì)調(diào)用 BuildOwner 的 buildScope 方法,方法里面會(huì)調(diào)用傳入的callback,也就是調(diào)用 element.mount(null, null);,而 mount 方法會(huì)循環(huán)創(chuàng)建子節(jié)點(diǎn),并在創(chuàng)建的過(guò)程中將需要更新的數(shù)據(jù)標(biāo)記為 dirty。 mount 方法里面會(huì)調(diào)用 updateChild 方法。

buildScope

image

該方法源碼太多,就不貼了,大概邏輯是先調(diào)用傳進(jìn)來(lái)的 callback 去循環(huán)創(chuàng)建子節(jié)點(diǎn),并在創(chuàng)建的過(guò)程中將需要更新的數(shù)據(jù)標(biāo)記為 dirty,然后循環(huán) _dirtyElements 數(shù)組,如果 Element 有 child 則會(huì)遞歸判斷子元素,并進(jìn)行子元素的 build ,創(chuàng)建新的 Element 或者修改 Element 或者創(chuàng)建 RenderObject。如果是首次渲染,則 _dirtyElements 是空的列表,因此首次渲染在該方法中是沒(méi)有任何執(zhí)行流程的。該方法的核心還是在第二次渲染或者 setState 后,有標(biāo)記 dirty 的 Element 時(shí)才會(huì)起作用。

updateChild

image

這個(gè)方法在 Flutter 的 Widget 系統(tǒng)中是非常重要的存在,每次在 Widget 樹(shù)中新增、修改、刪除一個(gè)子 Widget 都會(huì)調(diào)用這個(gè)方法去更新配置信息。所有子節(jié)點(diǎn)的處理都是經(jīng)過(guò)該方法,在該方法中 Flutter 會(huì)處理 Element(newWidget.createElement() Widget -> Element) 與 RenderObject(newChild.mount(this, newSlot) Element -> RenderObject) 的轉(zhuǎn)化邏輯,通過(guò) Element 樹(shù)的中間狀態(tài)來(lái)減少對(duì) RenderObject 樹(shù)的影響,從而提升性能。該方法的三個(gè)輸入?yún)?shù):Element child、Widget newWidget、dynamic newSlot :

  • child :當(dāng)前節(jié)點(diǎn)的 Element 信息
  • newWidget:Widget 樹(shù)的新節(jié)點(diǎn)
  • newSlot:新節(jié)點(diǎn)新位置

了解參數(shù)后,大概分析下方法的實(shí)現(xiàn):

  1. 判斷是否有新的 Widget 節(jié)點(diǎn),如果沒(méi)有,則將當(dāng)前節(jié)點(diǎn)的 Element 直接銷(xiāo)毀;

如果有,并且 Element 中也存在該節(jié)點(diǎn),繼續(xù)看下面邏輯

  1. 首先判斷當(dāng)前節(jié)點(diǎn)Element是否為空,為空則直接創(chuàng)建一個(gè)新的Element。不為空則繼續(xù)判斷當(dāng)前節(jié)點(diǎn) Element 中的舊 Widget 與新 Widget 是否一致,如果一致只是位置不同,則更新位置即可updateSlotForChild(child, newSlot);。其他情況下判斷是否可更新當(dāng)前節(jié)點(diǎn)Element 的 Widget,如果可以則更新child.update(newWidget);,如果不可以則銷(xiāo)毀當(dāng)前的 Element 節(jié)點(diǎn)deactivateChild(child);,并重新創(chuàng)建一個(gè)newChild = inflateWidget(newWidget, newSlot);。

在 child.update 函數(shù)邏輯里面,會(huì)根據(jù)當(dāng)前節(jié)點(diǎn)的類(lèi)型,調(diào)用不同的 update,具體如下四種:

  • StatelessElement#update
  • StatefulElement#update
  • MultiChildRenderObjectElement#update
  • SingleChildRenderObjectElement#update

上面四種類(lèi)型的 update 方法都會(huì)遞歸調(diào)用會(huì)遞歸調(diào)用子節(jié)點(diǎn),并循環(huán)返回到 updateChild 中。

有以下三個(gè)核心的函數(shù)會(huì)重新進(jìn)入 updateChild 流程中,分別是 performRebuild、inflateWidget 和 markNeedsBuild,接下來(lái)我們看下這三個(gè)函數(shù)具體的作用:

  • performRebuild
  • inflateWidget
  • markNeedsBuild

performRebuild

在 StatelessElement#update 和 StatefulElement#update 中會(huì)調(diào)用 rebuild 方法,進(jìn)而調(diào)用 performRebuild 方法,tatelessWidget 和 StatefulWidget 的 build 方法都是在此執(zhí)行,執(zhí)行完成后將作為該節(jié)點(diǎn)的子節(jié)點(diǎn),并進(jìn)入 updateChild 遞歸函數(shù)中。

inflateWidget

根據(jù)傳入的新的 widget 和位置創(chuàng)建一個(gè)新的 Element 節(jié)點(diǎn)作為當(dāng)前 Element 節(jié)點(diǎn)的子節(jié)點(diǎn),然后繼續(xù)調(diào)用新的 Element 節(jié)點(diǎn)的 mount 方法去循環(huán)子節(jié)點(diǎn),繼續(xù)調(diào)用 updateChild 重新進(jìn)入子節(jié)點(diǎn)更新流程。這里的 mount 方法根據(jù) Element 節(jié)點(diǎn)類(lèi)型不同有不同的實(shí)現(xiàn),當(dāng)為 RenderObjectElement 的時(shí)候還會(huì)去創(chuàng)建 RenderObject(Element -> RenderObject)。

markNeedsBuild

在 State#setState 方法里面調(diào)用;

把 Element 標(biāo)記為 dirty 并調(diào)用 BuildOwner#scheduleBuildFor 將其加入到臟列表(BuildOwner#_dirtyElements)中,等待下一次 buildScope (在下一次WidgetsBinding.drawFrame 時(shí)被調(diào)用來(lái)更新 Widget 樹(shù))。buildScope 中會(huì)遍歷 _dirtyElements 然后調(diào)用每個(gè)臟的 Element#rebuild 進(jìn)而調(diào)用 performRebuild 進(jìn)入 updateChild 流程。

3. 首次 build 流程

首次加載一個(gè)頁(yè)面時(shí),所有節(jié)點(diǎn)都不存在,此時(shí)的流程大部分情況下就是創(chuàng)建新的節(jié)點(diǎn),方法調(diào)用流程如下圖:

首次 build 流程

不管是首次加載還是再次加載,runApp 到 RenderObjectToWidgetElement#mount 邏輯都是一樣的,在 _rebuild 中會(huì)調(diào)用 updateChild 更新Element節(jié)點(diǎn),由于Element節(jié)點(diǎn)是不存在的,因此這時(shí)候就調(diào)用 inflateWidget 來(lái)創(chuàng)建 Element,然后調(diào)用 Element#mount,Element 實(shí)現(xiàn)類(lèi)主要分為:ComponentElement 和 RenderObjectElement。

ComponentElement

ComponentElement 主要子類(lèi):StatelessElememt、StatefulElement、InheritedElement。

當(dāng) Element 為 ComponentElement 時(shí),會(huì)調(diào)用 ComponentElement#mount ,在 ComponentElement#mount 中會(huì)創(chuàng)建 Element 并掛載到當(dāng)前節(jié)點(diǎn)上,繼續(xù)調(diào)用 _firstBuild 進(jìn)行子組件的 build ,build 完成后則將 build 好的組件作為子組件,進(jìn)入 updateChild 的子組件更新。

RenderObjectElement

RenderObjectElement的主要子類(lèi):SingleChildRenderObjectElement、MultiChildRenderObjectElement、RenderObjectToWidgetElement。

當(dāng) Element 為 RenderObjectElement 時(shí),則會(huì)調(diào)用 RenderObjectElement#mount,在 RenderObjectElement#mount 中調(diào)用 createRenderObject 創(chuàng)建 RenderObject,并將該 RenderObject 掛載到當(dāng)前節(jié)點(diǎn)的 RenderObject 樹(shù),最后同樣會(huì)調(diào)用 updateChild 或者 inflateWidget 來(lái)遞歸創(chuàng)建 Element 子節(jié)點(diǎn)。

4. setState 流程

上面有提到過(guò),在調(diào)用 State#setState 后,內(nèi)部會(huì)調(diào)用 markNeedsBuild 把 Element 標(biāo)記為 dirty 并調(diào)用 BuildOwner#scheduleBuildFor 將其加入到臟列表(BuildOwner#_dirtyElements)中,等待下一次 buildScope 。 buildScope 會(huì)調(diào)用 rebuild 然后進(jìn)入 build 操作,從而進(jìn)入 updateChild 的循環(huán)體系。整個(gè)流程圖如下:

setState 流程

上面流程圖中為了簡(jiǎn)化流程圖省略了 updateChild 中的部分條件,這里說(shuō)明一下:

  • 走綠色流程線的條件:除了圖中標(biāo)明的當(dāng)Element節(jié)點(diǎn)不存在以外,還有當(dāng) Element 節(jié)點(diǎn)存在,Widget 類(lèi)型相同,不是同一個(gè) Widget,并且 Widget 不可更新時(shí),依然是創(chuàng)建新的 Element 節(jié)點(diǎn)。
  • 走藍(lán)色流程線的條件:當(dāng) Element 節(jié)點(diǎn)存在,Widget 類(lèi)型相同,不是同一個(gè) Widget,并且 Widget 可更新時(shí),更新Element 節(jié)點(diǎn)中的 Widget 為新的 Widget。

由以上流程圖可知:Flutter 中 setState 調(diào)用后會(huì)引起當(dāng)前節(jié)點(diǎn)下的子節(jié)點(diǎn)遞歸 build,雖然不一定會(huì)造成 RenderObject 樹(shù)的改變,但也會(huì)存在一些性能影響。

5. 優(yōu)化

1. 針對(duì)調(diào)用 setState 會(huì)導(dǎo)致子 Widget rebuild 操作

  • 盡量使用 const Widget
  • 盡量減少 StatefulWidget 下的子 Widget ,也就是說(shuō) StatefulWidget 的粒度盡可能小

2. canUpdate 返回 false 導(dǎo)致創(chuàng)建新的 Elememt 節(jié)點(diǎn)

在 updateChild 的流程中會(huì)判斷 Element節(jié)點(diǎn)的 Widget 是否可更新,這段判斷的邏輯就在 Widget#canUpdate 里面,方法實(shí)現(xiàn)如下:

這里邏輯比較簡(jiǎn)單,主要判斷新舊 Widget 的運(yùn)行時(shí)類(lèi)型和 key 是否都相同,都相同則可以直接更新 Elememt 節(jié)點(diǎn)里面的 oldWidget 為 newWidget,而不是直接創(chuàng)建新的 Elememt 節(jié)點(diǎn),從而提升性能。優(yōu)化空間如下:

  • 盡量減少 Widget 的 key 的變化
  • 如果需要頻繁地對(duì)組件進(jìn)行排序、刪除或者新增處理時(shí),最好要給 Widget 增加 key ,以提升性能

使用 key 時(shí)注意點(diǎn):

由于 StatefulWidget 的 state 是保存在 Element 中,因此如果希望區(qū)分兩個(gè)相同類(lèi)名( runtimeType )的 Widget 時(shí),必須攜帶不同的 key ,否則會(huì)無(wú)法區(qū)分新舊 Widget 的變化。特別是在一個(gè)列表數(shù)據(jù),每個(gè)列表都是一個(gè)有狀態(tài)類(lèi),如果需要切換列表中項(xiàng)目列表時(shí),則必須設(shè)置 key,不然會(huì)導(dǎo)致順序切換失效。

3. 使用 GlobalKey 復(fù)用 Element

在 updateChild 中的 inflateWidget 中會(huì)檢查新傳入的 Widget 的 key 是否為 GlobalKey ,如果是則表明 Element 存在,那么這時(shí)候直接復(fù)用,如果不存在則需要重新創(chuàng)建,這其實(shí)就是 Widget 緩存,可以減少組件的 build 成本,inflateWidget 代碼如下:

使用 GlobalKey 會(huì)緩存 Widget,那么就會(huì)帶來(lái)一個(gè)問(wèn)題:內(nèi)存損耗。所以盡量在復(fù)用性高且 build 業(yè)務(wù)復(fù)雜的 Widget 上使用 GlobalKey。

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

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