框架
- Framework:一個純 Dart代碼的 SDK。它實現了一套基礎庫, 包含動畫、繪制和手勢處理。并基于繪制封裝了一套Widget控件庫,這套控件庫還根據 Material 和Cupertino兩種設計風格進行了風格化區分。
-
Engine:一個 C++實現的 SDK。其包含了 Skia引擎、Dart運行時、文字排版引擎等。在安卓上,系統自帶了Skia,在iOS上,則需要APP打包Skia庫,這會導致Flutter開發的iOS應用安裝包體積更大。 Dart運行時則可以以 JIT、JIT Snapshot 或者 AOT的模式運行 Dart代碼。
aHR0cHM6Ly9naXRlZS5jb20vYXJjdGljZm94MTkxOS9JbWFnZUhvc3RpbmcvcmF3L21hc3Rlci9pbWcvMV81TVk1eVJFclpjdjQ2bU4zZXI4SklBLnBuZw.png
其中 dart:ui庫是對Engine中Skia庫的C++接口的綁定。向上層提供了 window、text、canvas等通用的繪制能力,通過 dart:ui庫就能使用Dart代碼操作Skia繪制引擎。所以我們實際上可以通過實例化dart:ui包中的類(例如Canvas、Paint等)來繪制界面。然而,除了繪制,還要考慮到協調布局和響應觸摸等情況,這一切實現起來都異常麻煩,這也正是Framework幫我們做的事。
渲染層Rendering是在dart:ui庫之上的第一個抽象層,它為你做了所有繁重的數學工作(如跟蹤計算坐標等)。為了做到這一點,它使用RenderObject對象,該對象是真正繪制到屏幕上的渲染對象。由這些RenderObject組成的樹處理真正的布局和繪制。
在Engine之下,還包含一層Shell。這個單詞是 “殼”的意思,這個殼組合了Dart運行時、第三方工具庫、平臺特性等,實現在不同平臺調用和運行 Flutter應用。
總的來說, dart:ui給 Dart提供了繪制能力,Dart運行時為 Flutter提供了執行Dart代碼的能力,而Shell將他們組合起來,并且將生成的數據渲染到不同的平臺。
我們可以簡單的理解成,Skia 是打印機, Dart 運行時則是提供繪制的電源, dart: ui 提供紙張, Randering 是坐標系關系, 告訴打印機上下左右的, Text 是文字排版規范
注:
目前,程序主要有兩種運行方式:靜態編譯與動態解釋。
AOT: 靜態編譯的程序在執行前所有被翻譯為機器碼,一般將這種類型稱為AOT (Ahead of time compiler)即 “提前編譯”;如C、C++。
JIT:解釋執行的則是一句一句邊翻譯邊運行,一般將這種類型稱為JIT(Just-in-time)即“即時編譯”。如JavaScript、Python。
Dart中的JIT和AOT:
Dart在開發過程當中使用JIT,所以每次改都不須要再編譯成字節碼。節省了大量時間。
在部署中使用AOT生成高效的ARM代碼以保證高效的性能。
Dart 是少數同時支持 JIT(Just In Time,即時編譯)和 AOT(Ahead of Time,運行前編譯)的語言之一。
樹
那么講完Flutter框架的內容,我們來看看控件,也就是Flutter的樹, Flutter有Widget樹、Element樹、RenderObject樹,這是一般常說的三棵樹, 如果算上Layer, 就是四棵樹,它們各司其職,分成了幾個相關聯但清晰的結構
Widget
Widget 是 Flutter中UI開發的基本單元。 一個Widget里面通常存儲了視圖的配置信息,包括布局、屬性等。我們可以把它理解為一個UI元素的配置文件,類似于原生安卓開發中的xml描述文件。所謂Widget樹,就是我們手動編寫的結構化的Widget代碼,當被加載到內存時,就形成了Widget樹。
Element
Element 持有Widget和RenderObject兩者的引用,該對象實際上是一個上下文,將Widget與RenderObject映射關聯起來。通過遍歷Widget控件樹來構建一個Element樹結構。在原生開發中沒有對應的概念,它的概念更接近于Web前端中的虛擬DOM,主要做的事情也是比較前后兩次Widget的差異來決定如何更新真實的渲染對象樹(RenderObject樹)。
RenderObject
RenderObject 實際上需要渲染的樹,渲染引擎會根據RenderObject 來進行界面渲染。最接近原生開發中的UI控件元素。它主要處理UI構建過程中的布局與繪制。它依賴于Element樹來生成一棵RenderObject樹。
Layer
圖層對象。通常一棵RenderObject樹經過繪制之后,就會生成一個Layer對象,但并不是所有RenderObject都會繪制到一個Layer中,某些情況下,例如不同路由頁面,就會繪制到不同的Layer圖層中。這些Layer對象組成的結構就是Layer樹。
在繪制時,會根據 isRepaintBoundary是否為 true來決定是否繪制到新的圖層。了解這一點,我們就可以使用RepaintBoundary 控件在外層包裹,然后通過設置該控件的isRepaintBoundary屬性來提升繪制性能。因為 isRepaintBoundary 為 true 時,會形成了獨立的 Layer,這樣其他控件發生頻繁的改變時,就不會影響到獨立的圖層,這個獨立的圖層也不會發生重繪,節省性能開銷。
四者關系我們可以簡單的理解成:widget 是劇本, Element 是人, RenderObject 是皮偶, layer 是皮偶投射出來的影子,人通過劇本劇情來控制皮偶表演, 投射成一個皮影戲了
樹的創建
- 創建widget樹
-
調用runApp(rootWidget),將rootWidget傳給rootElement,做為rootElement的子節點,生成Element樹,Framework 調用 element.mount(parentElement,newSlot) ,mount方法中首先調用element所對應Widget的createRenderObject方法創建與element相關聯的RenderObject對象,然后調用element.attachRenderObject方法將element.renderObject添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發生在Element樹結構發生變化時才需要重新attach)。插入到渲染樹后的element就處于“active”狀態,處于“active”狀態后就可以顯示在屏幕上了
866f32c865e3313cd029ae1f988a1a32.png.jpeg
樹的更新
找到widget對應的element節點,設置element為dirty,最終會將需要更新的element加入到_dirtyElements的列表中,_dirtyElements是一個集合,存儲了所有標記為“臟”的節點。在對其中的“臟”節點進行處理時,需要首先對集合中的“臟”節點進行排序,其排序規則如下:
- 如果“臟”節點的深度不同,則按照深度進行升序排序
- 如果“臟”節點的深度相同,則會將“臟”節點放在集合的右側,“干凈”節點則在在集合的左側。
- 在排序完成后,就要遍歷該集合,對其中的“臟”節點進行處理。在這里調用的是rebuild函數,通過該函數,會重新創建“臟”節點下的所有Widget對象,并根據新的Widget對象來判斷是否需要重用Element對象。一般只要不是增刪Widget,Element對象都會被重用,從而也就會重用RenderObject對象。
- 這里要注意一點的是,如果_dirtyElements中的“臟”節點還未處理完畢,就又新增了“臟”節點,那么這時候就會重新排序,保證_dirtyElements集合的左側永遠是“干凈”節點,右側永遠是“臟”節點。
在rebuild函數中會調用performRebuild函數,該函數是一個抽象函數,在其子類實現,而標記為“臟”的Element都是StatefulElement。所以就來StatefulElement或者其父類中查找performRebuild函數。
performRebuild函數做的事很簡單,就是創建新的Widget對象來替換舊的對象。再調用Element的updateChild函數,更新Element對應的Widget對象。而在updateChild函數中又會調用子Element的update函數,從而調用子Element的performRebuild,然后在調用子Element的updateChild、update函數。以此類推,從而更新其所有子Element的Widget對象。
最后就是調用葉子節點的updateRenderObject函數來更新RenderObject。在更新RenderObject對象時,會根據情況來對需要重新布局及重新繪制的RenderObject對象進行標記。然后等待下一次的Vsync信號時來重新布局及繪制UI。
updateChild函數中分了幾種情況
- 不存在原始child,則新創建新的widget,并重置element,關聯renderobject
- 如果新舊控件的類型相同,并且控件也相同,直接更新wiget
- 如果新舊控件的類型相同,并且這個wiget 能復用(即widget.canupdate為true),則更新widget,并執行updateRenderObject
- 如果新舊控件類型不同,則移出原有widget,并重新創建新的widget,并重置element,關聯renderobject
樹的作用
應該說多棵樹結構的作用,簡而言之是為了性能,為了復用Element從而減少頻繁創建和銷毀RenderObject。因為實例化一個RenderObject的成本是很高的,頻繁的實例化和銷毀RenderObject對性能的影響比較大,所以當Widget樹改變的時候,Flutter使用Element樹來比較新的Widget樹和原來的Widget樹,element對widget樹的變化做了抽象,可以只將真正變化的部分同步給RenderObject進行刷新,這樣就能提高渲染效率,而不是重新構建整個widget,對變化前后的數據進行比較,告訴render哪些是需要重新渲染的
注:
關于更新時,Element的變化:
當有父Widget的配置數據改變時,同時其State.build返回的Widget結構與之前不同,此時就需要重新構建對應的Element樹。為了進行Element復用,在Element重新構建前會先嘗試是否可以復用舊樹上相同位置的element,element節點在更新前都會調用其對應Widget的canUpdate方法,如果返回true,則復用舊Element,舊的Element會使用新Widget配置數據更新,反之則會創建一個新的Element。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時相等,如果同時相等就返回true,否則就會返回false。根據這個原理,當我們需要強制更新一個Widget時,可以通過指定不同的Key來避免復用。
當有祖先級Element決定要移除element 時(如Widget樹結構發生了變化,導致element對應的Widget被移除),這時該祖先級Element就會調用deactivateChild 方法來移除它,移除后element.renderObject也會被從渲染樹中移除,然后Framework會調用element.deactivate 方法,這時element狀態變為“inactive”狀態。
“inactive”態的element將不會再顯示到屏幕。為了避免在一次動畫執行過程中反復創建、移除某個特定element,“inactive”態的element在當前動畫最后一幀結束前都會保留,如果在動畫執行結束后它還未能重新變成“active”狀態,Framework就會調用其unmount方法將其徹底移除,這時element的狀態為defunct,它將永遠不會再被插入到樹中。
如果element要重新插入到Element樹的其它位置,如element或element的祖先級擁有一個GlobalKey(用于全局復用元素),那么Framework會先將element從現有位置移除,然后再調用其activate方法,并將其renderObject重新attach到渲染樹。
布局過程
Flutter 中的控件在屏幕上繪制渲染之前需要先進行布局(Layout)操作。其具體可分為兩個線性過程:
從頂部向下傳遞約束。
這一過程用于傳遞布局約束。父節點給每個子節點傳遞約束,這些約束是每個子節點在布局階段必須要遵守的規則。常見的約束包括規定子節點最大最小寬度或者子節點最大最小的高度。這種約束會向下延伸,子組件也會產生約束傳遞給自己的子節點,一直到葉子結點。
從底部向上傳遞布局信息。
這一過程用來傳遞具體的布局信息。子節點接受到來自父節點的約束后,會依據它產生自己具體的布局信息,如父節點規定我的最小寬度是 500 的單位像素,子節點按照這個規則可能定義自己的寬度為 500 個像素,或者大于 500 像素的任何一個值。這樣,確定好自己的布局信息之后,將這些信息告訴父節點。父節點也會繼續此操作向上傳遞一直到最頂部。 其過程可用下圖表示:
渲染流程
上面內容是從控件和布局層面來說頁面更新的,而宏觀流程上,當需要更新頁面的時候,由應用上層通知到Engine,Engine會等到下個Vsync信號到達的時候,去通知Framework上層,然后Framework會進行Animation, Build,Layout,Compositing,Paint,最后生成layer提交給Engine。Engine會把layer進行組合,生成紋理,最后通過OpenGl接口提交數據給GPU, GPU經過處理后在顯示器上面顯示
合成與光柵化
所有圖層都交由 GPU 來負責合成并上屏顯示。在渲染流程的最后兩個步驟中,正是合成與光柵化。
合成
就是把所有layer組合成Scene,然后通過 ui.window.render 方法,把 scene提交給Engine,到這一步,Framework向Engine提交數據基本完成了
光柵化
合成已經理解了,那么什么是光柵化呢?
光柵化也稱柵格化,是指將幾何數據經過一系列變換后最終轉換為像素,從而呈現在顯示設備上的過程。光柵化的本質是坐標變換、幾何離散化。
iOS與Flutter渲染對比
iOS
- 更新視圖樹,同步更新圖層樹。
- CPU 計算要顯示的內容、圖像解碼轉換。當runloop 在BeforeWaiting和Exit時,會通知注冊的監聽,然后對圖層打包,打完包后,將打包數據發送給一個獨立負責渲染的進程Render Server。
- Render Server 將數據反序列化,得到圖層樹。按照圖層樹中圖層順序、RGBA 值、圖層 frame 過濾圖層中被遮擋的部分,過濾后將圖層樹轉成渲染樹,渲染樹的信息會轉給 OpenGL ES/Metal。
- Render Server 會調用 GPU,GPU 開始進行頂點著色器、形狀裝配、幾何著色器、光柵化、片源著色器、測試與混合六個階段。
- 將GPU渲染結果放到幀緩沖區,當下個Vsync信號時,從幀緩沖區取出放到屏幕。
flutter
Flutter由Skia繪制引擎提供提供圖形繪制能力,向GPU提供數據。(替代了iOS中的Core Graphics、Core Animation、Core Text)