element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject
Flutter 框架整體分層:
下面會主要關注 Widgets 和 Rendering, 也會涉及到一點 Painting 部分的內容。
Flutter 三層樹
React 中,Component 對象并不是真正負責渲染,需要框架生成 element 后,進行初始渲染或者 diff 后 patch 變更。Flutter 也借鑒了 React 的設計,Widget 并不是真正的渲染對象,真正的渲染是經歷了從?Widget?=>?Element=>?RenderObject?的過程,與 React 拼接成 dom 交給瀏覽器渲染不同,Flutter 中 RenderObject 負責來在繪制引擎的上層進行繪制。
先來看下類分布及其職責:
-- Widget 存儲配置信息,另外其由于是 immutable 的,所以會不斷重新創建刷新。-- Element 是分離 Widget 和真正渲染對象的中間層,很多控制渲染的行為在這層去處理。-- RenderObject 來真正的執行 Diff, Hit Test, 布局以及繪制。
三層對象構成的樹之間的關聯則如下圖表示:
如圖可知,Widget 會創建 Element,然后 Element 創建相應的 RenderObject,Element 就是 Widget 在 UI 樹具體位置的一個對象,一個 Widget 可能有多個 Element,大多的 Element 只有唯一的 renderObject,但也有一些 Element 會有多個子節點,比如 MultiChildRenderObjectElement。最終所有 RenderObject 構成 render tree 。
Stateful & Stateless Widget
關于 StatefulWidget 以及 StatelessWidget 的使用,已經有很多的文章進行了描述,這些 Widget 可能不是傳統原生開發中的 UI 控件,也可能是布局,語義化或者主題等等,通過這些組件組合堆疊,來完成應用界面的繪制。
在一開始接觸 Flutter 時會有點抵觸這個 State 職責定位,因為涉及到構建樹的 build 方法也需要在 State 中重載。不過整體看下來,這么設計也是基于 Flutter 的一部分機制導致的,首先 Widget 本身也分為 UI 和功能型,另外 Widget 樹會隨著 State 的變化而變化,每次 build 都會重新生成樹,Widget 會頻繁的銷毀重建而 State 對象則不會,所以這么看來由 State 對象來返回樹會更合適一些。
State 的生命周期
initState 方法會在 State 初始化時調用,由于 State 對象會被 Framework 長期持有,所以該方法在其生命周期中只調用一次,我們經常會在這做一些一次性的操作,比如狀態初始化,訂閱事件等。
didChangeDependencies 這個方法會在初始化后調用一次,直到 State 對象的依賴發生變化時才會被調用,比如上層的樹中包含 InheritedWidget,如果其發生了變化,那么此時 InhertiedWidget 的子 widget 的 didChangeDependencies() 都會被調用。典型的場景是一些全局的配置比如 Locale, Theme, 或者 Redux 的 StoreProvider 組件改變會導致子樹收到該消息并 re-build 組件樹。另外如果有些行為不希望在每次 build 都觸發,也可以考慮到將其放到 didChangeDenpendencies 中來。
didUpdateWidget 當 widget 的配置變化時,會調用該方法并觸發 build
build 返回改組件構建的樹結構
deactivate 當前組件暫時被從視圖樹中移除時會調用該方法,比如頁面切換或者應用掛起都會觸發這個方法。
dispose 永久移除時作為析構函數,可以在這里做一些資源的釋放操作。
上面有提到 InheritedWidget,這個類的存在主要來解決需要逐層傳遞 State 的問題,當我們有 State 需要共享時,就可以將其放在一個繼承 InheritedWidget 的類中,然后使用的組件直接取用就可以。常見與其相關的場景比如 Theme.of(context), Locale.of(context), ButtonTheme.of(context) 等等。
Element
Element 的生命周期
Widget.createElement?創建一個 Element 實例。
element.mount()?會讓其 widget?createRenderObject?并將對象 attach 到渲染樹中插槽指定的位置,插入后該 element 標記為 'active' 狀態。
當 widget 的配置數據改變時,為了對 element 進行復用,Framework 在決定重新創建 Element 前會先嘗試復用相同位置舊的 element, 調用對應的 widget 的 canUpdate() 方法來確定是否更新,canUpdate()方法主要判斷新舊 widget 的 runtimeType 以及 key, 所以我們可以通過指定不同的 Key 來強制刷新。
當有祖先元素決定要移除 element 時,會調用?deactivateChild?方法來移除孩子,移除后?element.renderObject?也會被從渲染樹中移除,然后 Framework 會調用?element.deactivate?方法,這時 element 標記為 'inactive' 狀態。
'inactive' 態的 element 將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定 element, 'inactive' 態的 element 在當前動畫最后一幀結束前都會保留,如果動畫結束后不能重新 'active', 則會調用?unmount?方法將其徹底移除,這是 element 狀態標為 defunct 。
如果 element 要重新插入到 Element 樹其它位置,如 element 或 element 的祖先擁有一個 GlobalKey, 那么 Framework 會先將 element 從現有位置移除,然后再調用?activate?方法,并將其 renderObject 重新 attach 到渲染樹。
BuildContext
我們在 build widgets 時都會重載 build 方法,build 方法有一個參數就是 BuildContext 上下文對象,我們可以拿到 context 去其祖先尋找指定類型的對象,比如 Theme, Navigator, Localizations 等等。
文檔表示的很清楚,BuildContext 對象實際上就是 Element 對象, 它設計成抽象接口類主要為了屏蔽使用者對 Element 進行操作。而我們常用的?of?操作則是調用了 Element 的?inheritFromWidgetOfExactType()?方法。以?Theme.of(context)?為例,我們可以看到它的實現:
這里會調用 context 也就是 element 的 inheritFromWidgetOfExactType 方法拿到指定類型的對象,返回一個經過本地化的主題數據。
inheritFromWidgetOfExactType 方法也很簡單,直接從緩存的哈希表里找到擁有指定對象類型的祖先節點,然后返回給調用方,緩存中沒有則添加到緩存中。另外值得注意的是,這里的哈希表就是之前在 Widget state 里提到的 dependencies 。
BuildOwner
上文看到 BuildContext 中有一個 getter 方法來獲取 buildOwner, 那么 BuildOwner 具體會做些什么呢?
它作為 widgets 的管理者,會追蹤哪些 widget 需要重建,并且處理一些組件樹上的其他任務比如管理 inactive 的 element 列表,或者觸發 reassemble 命令當 hot reload 的時候。最主要的 build owner 是由 WidgetsBinding 持有的,它會被操作系統驅動去執行 build/layout/paint 的 pipeline。
另外 build owners 也被用來管理 off-screen 的組件樹,谷歌官方介紹組件樹的視頻有提到,每當 widget 改變需要重新渲染時,framework 會在繪制的 idle time 去計算要新渲染的樹,完成后直接對當前的進行替換并渲染。利用 idle time 的方式也很類似 React 的 Fiber 設計 (利用瀏覽器 requestIdleCallback api)。
布局及繪制
Flutter 界面渲染過程分為三個階段:布局,繪制和合成,布局和繪制會在 Flutter 框架中完成,而合成則交給引擎負責:
前文有提到每個 Element 都會對應一個 RenderObject,它的職責主要則是布局和繪制,所有的 RenderObject 會組成一棵渲染樹 RenderTree 。
RenderObject 擁有一個 parent 和一個 parentData 插槽,parentData 這個預留變量正是由 parent 來賦值,parent 通常會通過子 RenderObject 來存儲一些和子元素相關的數據比如偏移量。當然,其不僅僅可以存儲偏移信息,通常所有和子節點特定的數據都可以存儲到子節點的 parentData 中,如?ContainerBox?中該屬性就保存了指向兄弟節點的?previousSibling?和?nextSibling,Element 的?visitChildren()?方法也是通過它們來實現對子節點的同層級(廣度)遍歷。
RenderObject 類本身實現了一套基礎的 layout 和繪制協議,但是并沒有定義子節點模型,坐標系統以及具體的布局協議。為此,Flutter 提供了一個 RenderBox 類,繼承自 RenderObject,坐標系采用笛卡爾坐標系。
布局過程
渲染樹種每個節點都會接受父節點的 Contraints 參數,決定自己大小,然后父節點就可以按照自己的邏輯決定各個子節點的位置,完成布局過程。
具體來看,RenderBox 中有一個 size 屬性用來保存寬高,RenderBox 的 layout 是通過在組件樹上從上往下傳遞 BoxConstraints 對象實現的,它可以限制子節點的最大和最小寬高,布局階段,父節點會調用子節點的?layout()?方法,大致實現如下:
布局前先要確定?relayoutBoundary,該參數標識當前節點是否是布局邊界。即當邊界內的節點發生重新布局時,不會影響邊界外的節點。
在 Element 層,如果其被標記為 dirty 時(通過?markNeedsBuild())則會重新 build,這時 RenderObject 便會重新布局。在 RenderObject 中則有一個?markNeedsLayout()?方法,它會將 RenderObject 的布局狀態標記為 dirty,這樣在下一幀便會重新 layout,我們來看下該方法:
確定?relayoutBoundary?是不是自己,不是則繼續向上尋找,是則告知 buildOwner 當前節點需要布局,并調用了更新方法。?
另外在?layout()?方法中我們看到其是通過?performLayout()?來去真正的執行布局,則總結起來調用順序為:?layout() > performResize()/perforLayout() > child.layout() > ...?如此遞歸完成整個 UI 樹的布局。如果需要子類化 RenderBox 類來定制布局,則應該通過重寫?performResize?和?performLayout?來實現,而不是?layout?。
另外有一個問題,除非顯式的指定 size,很多控件在 build 時是不清楚具體尺寸的,但很多時候我們需要提前清楚 size 來做一些操作或者布局,閑魚團隊的?深入了解Flutter界面開發?中有提到可以在 layout 階段后發送一個 notification 通知上層,另外也可以并推薦的方式是通過膠水層 WidgetsBinding 注冊一個 PostFrameCallback (WidgetsBinding.instance.addPostFrameCallback),然后回調里通過
_listViewKey.currentContext.findRenderObject().paintBounds.size.width;
的方式來拿到尺寸。
繪制過程
RenderObject 可以通過 paint() 方法來完成具體繪制邏輯,流程和布局類似。這里以 RenderFlex 的 paint 方法為例說明:
defaultPaint:
由于 Flex 屬于一個布局類,自身沒有需要繪制的部分,則直接遍歷子節點并調用 paintChild 方法觸發子節點繪制。
渲染流程中也有個與 relayoutBoundary 對應的屬性 repaintBoundary,用于確定重繪邊界,提高繪制效率,避免繪制的干擾以及不必要的重繪。
RenderObject 中有一個?isRepaintBoundary?屬性,決定重繪時是否獨立于其父元素,若為 true 則單獨建立圖層繪制??梢钥聪?paintChild()?方法:
如果子節點是 repaintBoundary 則會調用?_compositeChild?方法并將偏移量傳遞過去,不是則直接從上下文進行繪制。
另外看下觸發重繪的?markNeedsPaint()?方法:
與 layout 類似,會判斷邊界并交給 pipeline owner 去做相關工作。
在 iOS 中,不同職責的 layer 組合在一起組成了 view,Flutter 中剛才可以看到如果有一個 repaintBoundary 區域形成時,框架會創建一個 Layer,不同 Lyaer 也是可以獨立工作的,比如 OffsetLayer 在 RenderObject 中就是用來做定位繪制的。
其次在 RenderObject 中有一個屬性為 needsCompositing,它會影響生成多少層的 Layer, 而這些 Layer 又會組成一棵 Layer Tree ... 也就是實際去給引擎繪制的樹。
實現一個 Button
最后,我們會以框架實現 Button 的方式來實現一個帶漸變背景的按鈕:
語義化在最頂層,使用?Semantics?來包裝整個 Widget;管理焦點(1.7 新增)使用?Focus;布局和焦點用到兩個 'Box',分別為?ConstrainedBox?和?DecoratedBox;樣式及點擊水波效果通過?Material?和?Inkwell;Default 的樣式則通過全局的?Theme,?ButtonTheme?和?IconTheme?來控制;
綜上,build 方法大概如下:
-- EOF --以上為本篇文章的全部內容,歡迎提出建議和指正,
參考資源:
Github/flutter
flutter
RenderObject和RenderBox
深入了解Flutter界面開發
Flutter的原理及美團的實踐
深入繪制原理
基于JS的高性能Flutter動態化框架MXFlutter
flutter-playlist-youtube
總結
以上是生活随笔為你收集整理的element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 广数系统980tdb系统说明书_汽车离合
- 下一篇: ue4相机_纳格数字创意课程介绍 |UE