天天看點

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Flutter架構簡介

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語
移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

跨平台應用的架構,沒有使用WebView或者系統平台自帶的控件,使用自身的高性能渲染引擎(Skia)自繪。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

界面開發語言使用dart,底層渲染引擎使用C, C++。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

組合大于繼承,控件本身通常由許多小型、單用途的控件組成,結合起來産生強大的效果,類的層次結構是扁平的,以最大化可能的組合數量。

Rendering Pipeline

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

本文主要介紹build、layout、paint的三個階段。

視圖樹

Widget&Element&RenderObject

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Flutter視圖樹包含了三種樹,上圖隻是介紹了三顆樹的基礎class的對應關系和功能介紹。

建立樹

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

建立widget樹

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

調用runApp(rootWidget),将rootWidget傳給rootElement,做為rootElement的子節點,生成Element樹,由Element樹生成Render樹

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語
移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Widget:存放渲染内容、視圖布局資訊,widget的屬性最好都是immutable(如何更新資料呢?檢視後續内容)

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Element:存放上下文,通過Element周遊視圖樹,Element同時持有Widget和RenderObject

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

RenderObject:根據Widget的布局屬性進行layout,paint Widget傳人的内容

更新樹

為什麼widget都是immutable?

Flutter界面開發是一種響應式程式設計,主張simple is fast,Flutter設計的初衷希望資料變更時發送通知到對應的可變更節點(可能是一個StatefullWidget子節點,也可以是rootWidget),由上到下重新create widget樹進行重新整理,這種思路比較簡單,不用關心資料變更會影響到哪些節點。

widget重新建立,element樹和renderObject樹是否也重新建立?

widget隻是一個配置資料結構,建立是非常輕量的,加上Flutter團隊對widget的建立/銷毀做了優化,不用擔心整個widget樹重新建立所帶來的性能問題,但是renderobject就不一樣了,renderobject涉及到layout、paint等複雜操作,是一個真正渲染的view,整個view 樹重新建立開銷就比較大,是以答案是否定的。

樹的更新規則

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

找到widget對應的element節點,設定element為dirty,觸發drawframe, drawframe會調用element的performRebuild()進行樹重建

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

widget.build() == null, deactive element.child,删除子樹,流程結束

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

element.child.widget == NULL, mount 的新子樹,流程結束

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

element.child.widget == widget.build() 無需重建,否則進入流程5

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Widget.canUpdate(element.child.widget, newWidget) == true,更新child的slot,element.child.update(newWidget)(如果child還有子節點,則遞歸上面的流程進行子樹更新),流程結束,否則轉6

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Widget.canUpdate(element.child.widget, newWidget) != true(widget的classtype 或者 key 不相等),deactivew element.child,mount 新子樹

注意事項:

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

element.child.widget == widget.build(),不會觸發子樹的update,當觸發update的時候,如果沒有生效,要注意widget是否使用舊widget,沒有new widget,導緻update流程走到該widget就停止了。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

子樹的深度變化,會引起子樹重建,如果子樹是一個複雜度很高的樹,可以使用GlobalKey做為子樹widget的key。GlobalKey具有緩存功能。

如何觸發樹更新

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

全局更新:調用runApp(rootWidget),一般flutter啟動時調用後不再會調用。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

局部子樹更新, 将該子樹做StatefullWidget的一個子widget,并建立對應的State類執行個體,通過調用state.setState() 觸發該子樹的重新整理。

Widget

StatefullWidget vs StatelessWidget

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

StatelessWidget:無中間狀态變化的widget,需要更新展示内容就得通過重新new,Flutter推薦盡量使用StatelessWidget。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

StatefullWidget:存在中間狀态變化,那麼問題來了,widget不是都immutable的,狀态變化存儲在哪裡?Flutter 引入state的類用于存放中間态,通過調用state.setState()進行此節點及以下的整個子樹更新。

State 生命周期

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

initState(): state create之後被insert到tree時調用的

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

didUpdateWidget(newWidget):祖先節點rebuild widget時調用

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

deactivate():widget被remove的時候調用,一個widget從tree中remove掉,可以在dispose接口被調用前,重新instert到一個新tree中

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

didChangeDependencies():

  • 初始化時,在initState()之後立刻調用
  • 當依賴的InheritedWidget rebuild,會觸發此接口被調用
移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

build():

  • After calling [initState].
  • After calling [didUpdateWidget].
  • After receiving a call to [setState].
  • After a dependency of this [State] object changes (e.g., an[InheritedWidget] referenced by the previous [build] changes).
  • After calling [deactivate] and then reinserting the [State] object into the tree at another location.
移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

dispose():Widget徹底銷毀時調用

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

reassemble(): hot reload調用

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

A頁面push一個新的頁面B,A頁面的widget樹中的所有state會依次調用deactivate(), didUpdateWidget(newWidget)、build()(這裡懷疑是bug,A頁面push一個新頁面,理論上并沒有将A頁面進行remove操作),當然從功能上,沒有看出來有什麼異常。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

當ListView中的item滾動出可顯示區域的時候,item會被從樹中remove掉,此item子樹中所有的state都會被dispose,state記錄的資料都會銷毀,item滾動回可顯示區域時,會重新建立全新的state、element、renderobject。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

使用hot reload功能時,要特别注意state執行個體是沒有重新建立的,如果該state中存在一下複雜的資源更新需要重新加載才能生效,那麼需要在reassemble()添加處理,不然當你使用hot reload時候可能會出現一些意想不到的結果,例如,要将顯示本地檔案的内容到螢幕上,當你開發過程中,替換了檔案中的内容,但是hot reload沒有觸發重新讀取檔案内容,頁面顯示還是原來的舊内容。

資料流轉

從上往下

資料從根往下傳資料,正常做法是一層層往下,當深度變大,資料的傳輸變的困難,Flutter提供InheritedWidget用于子節點向祖先節點擷取資料的機制,如下例子:

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

child及其以下的節點可以通過調用下面的接口讀取color資料:

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

說明:BuildContext 就是Element的一個接口類

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

context.inheritFromWidgetOfExactType(FrogColor)其實是通過context/element往上周遊樹,查找到第一個FrogColor的祖先節點,取該節點的widget對象。

從下往上

子節點狀态變更,向上上報通過發送通知的方式

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

定義通知類,繼承至Notification

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

父節點使用NotificationListener 進行監聽捕獲通知

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

子節點有資料變更調用下面接口進行資料上報

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

閑魚Flutter的界面架構設計

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Layout

Size 計算

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

parent傳入限制條件,在dramframe的layout階段,child根據自身的渲染内容傳回size。

問題:在build()階段擷取不到size,很多時候需要提前知道部分widget size來進行布局,解決方案當widget 在對應renderobject的layout階段之後,發送一個LayoutChangeNotification,參考SizeChangedLayoutNotifier class,但是SizeChangedLayoutNotifier沒有上報init layout size,可以自己參考這個實作封裝一個Notifier。

Offset 計算

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

renderObject拿到計算好的size,再加上一些布局屬性(align、paddig)等,計算child相對parent的offset。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

offset存放在每個child renderObject的BoxParentData中。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

當parent擁有mutil children時,BoxParentData還用來存children兄弟節點之間的周遊順序。

Relayout boundary

renderObject在layout階段做了Relayout boundary的優化,當子樹進行relayout時,滿足下面三種中的一種:

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

parentUsesSize == false

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

sizedByParent == true

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

constraints.isTight

那麼該renderObject設定為Relayout boundary,也就是該renderObject的重新layout不觸發parent的layout,一般情況下開發人員不需要關心Relayout boundary,除非是使用CustomMultiChildLayout。

Paint

Layer

iOS的每一個UIView都有一個layer,Flutter的render object不一定存在layer,一般情況下一個renderObject子樹都渲染在一個layer上,那麼什麼renderObject具有layer,子renderObject怎麼渲染到這個layer?

1.當一個renderObject的

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

或者

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

,renderOject會有對應的compositing layer。

2.子renderObject會對目标layer傳回對應的offsetLayer, 目标compositing layer再根據offset合成一個渲染的紋理buffer。

移動開發新利器 | 一文深入了解 Flutter 界面開發Flutter架構簡介Rendering Pipeline視圖樹Widget結語

Repaint Boundary

類似Relayout boundary,Paint階段也有Repaint Boundary,目的和layout一樣,就是對應子樹的paint不會導緻外部的repaint,但是Relayout boundary需要開發人員自己設定,使用RepaintBoundary widget進行設定,ListView在渲染的item預設都是使用了RepaintBoundary,顯而易見ListView的children之間都是互相獨立的。Flutter建議複雜的image渲染使用RepaintBoundary,image的渲染需要io操作,然後解碼,最後渲染,使用RepaintBoundary可以進行gpu的緩存,但是不一定就會緩存,engine會判斷這個image是否足夠複雜,畢竟gpu緩存還是非常珍貴的,同時RepaintBoundary還會對一些反複渲染的layer進行緩存處理(反複渲染3次及以上,這個是Flutter的視訊中提到的)。

結語

Flutter還處于Beta階段,有些界面程式設計的接口設計還不夠成熟,相比iOS和安卓生态還很不成熟,需要我們共同的建立,Flutter提供的調試工具相比一開始接觸的時候,已經完善很多,讓我們給Flutter更多的耐心和包容,期待Flutter越來越完善。

原文釋出時間為:2018-06-6

本文作者:閑魚技術團隊

本文來自雲栖社群合作夥伴“

阿裡技術

”,了解相關資訊可以關注“

”。