之前,寫了一篇《iOS 淺談GPU及“App渲染流程”》闡述了iOS端App的渲染流程。
其中包括三種渲染方式,分別是:
1.iOS原生渲染:使用原生控制元件、原生語言編寫。
2.大前端渲染(一種是WebView
,另一種是JSCore
引擎作為虛擬機器)。
3.Flutter渲染。
《上篇部落格》主要講解了iOS APP
渲染的流程,以及GPU的渲染流水線。
但關於Flutter是如何渲染的?我上篇寫的並不是很透徹。
所以本篇將和大家一起談談 —— “ Flutter 究竟是如何渲染的? ”
一、Flutter 的架構
先從Flutter的整體架構說起,共分為三層,又下到上分別為:Embedder
層、Engine
層、Framework
層。
分別對應:
- Embedder層:作業系統底層適配層。
- Engine層:渲染引擎層。
- Framework層:用Dart實現的上層UI SDK。
那麼,這三層的職責是什麼?究竟分別做了哪些事呢?
從上到下開始,先從我們相對比較熟悉的 Framework
層說起。
1. Framework層:Dart實現的上層UI SDK
實現了:Animation
(動畫)、Painting
(圖形繪製)、Gestures
(手勢操作)等功能,幷包裝成對應的 api
提供給上層開發者呼叫。
為了保證Flutter所繪製的控制元件與原生控制元件風格類似,Flutter封裝了Material
(對應Android
)、Cupertino
(對應iOS
)風格的UI元件庫,供開發者直接使用。
2. Engine層:Skia渲染 + DartVM 引擎
這層主要包含三塊:Skia
、Dart
、Text
。
Skia
是渲染引擎,為 Framework
層提供“底層渲染”能力。
Dart
是 Dart
執行時引擎,為 Framework
層提供執行時呼叫Dart和渲染能力。
Text
是文字排版,為 Framework
層提供檢視排版能力。
3. Embedder層:作業系統適配
對不同平臺作業系統的適配,包括一些配置:surface、執行緒、外掛等特性。
由於Flutter相關特性並不多,因此對不同平臺作業系統的適配成本很低。
二、Flutter 的 渲染策略
我們都知道,在Flutter中,Everything is widget
。
所有 Widget
會組成 Widget Tree
。
介面更新時,會更新 Widget Tree
,
再更新 Element Tree
,最後更新 RenderObjectTree
。
那麼,究竟Flutter是怎麼更新介面的?又有哪些優化的渲染策略呢??
分為4個階段,分別是:
佈局階段 => 繪製階段 => 合成階段 => 渲染階段
(Layout
=> Paint
=> Composite
=> Rasterize
)
1. 佈局(Layout)
Flutter採用 “深度優先” 機制遍歷Widget Tree。
我們都知道,在佈局過程中,Widget樹中的每個孩子節點都會受到父節點所加的位置約束。(也就是說,父節點決定孩子節點的位置,孩子節點只能決定自己的大小。)
先確定父節點的大小、位置,
再確定孩子節點的位置,
再確定孩子節點的大小。
....一直往下,
直到沒有孩子Widget
後,返回完成。
PS:深度優先於廣度優先的區別:參考部落格
為了防止孩子節點的變化,導致整個 Widget Tree
重新佈局。
Flutter加入了 “佈局邊界” 機制(Relayout Boundary)。(劃重點,★優化點★)
在某些節點,“自動”或“手動”加上 “佈局邊界” ,控制邊界。
在該佈局邊界內的任何節點發生重新佈局,都不會影響邊界外的 Widget Tree
的佈局。
佈局完成後,樹上每個節點都確定了“尺寸大小”和“位置”。
2. 繪製(Paint)
剛才,我們確定了樹上的控制元件的“尺寸”與“位置”。
接下來是繪製階段。
和佈局類似,Flutter也是採用 “深度優先” 機制遍歷渲染樹。
先繪製自身,再繪製子節點。
但是,會出現一些繪製圖層覆蓋問題。
比如,當節點2需要重繪時,節點2、5、6一起重繪了(其實只要重繪節點2、5)。
為了解決繪製覆蓋問題,Flutter採用了也是和佈局階段相似的策略: 重繪邊界 機制(Repaint Boundary)。
其實,本質上就是加個新的圖層,避免在同一圖層重繪產生影響。(劃重點,★優化點★)
典型的例子是,ScrollView。
一旦設定好重繪邊界,滾動時,只會重繪ScollView中的檢視內容,而其他部分不用重新繪製。(劃重點,★優化點★)
3. 合成(Composite)
由於繪製出來的渲染樹,會有很多層,同步多層渲染會出現效能問題。
因此,Flutter會在渲染前,將多個渲染樹圖層進行合成。(劃重點,★優化點★)
根據多層渲染樹的大小、層級、透明度等計算後,
合成為最終“簡化版”的渲染樹,以提高下一步的渲染效率。
4. 渲染(Rasterize)
將處理過的“簡化版”渲染樹,交給Skia
引擎轉換成“二維影像資料”。
然後 Skia
把計算好的圖形資料,通過 OpenGL
介面交給 GPU
渲染,走上一篇《iOS 淺談GPU及“App渲染流程”》中提到的 GPU
工作流水線:
頂點著色器 => 形狀裝配 => 幾何著色器 => 光柵化 => 片段著色器 => 測試與混合。
然後,GPU工作流水線六階段完成。
最終,展示到我們的終端螢幕上。
當然這只是一個垂直同步訊號(VSync)的過程。(按60fps算,一秒需要60個VSync才不會感到卡頓。)
三、Flutter 渲染工作流水線
按60fps算,一秒需要60個VSync才不會感到卡頓。
在流水線的Widget更新,Flutter也有優化的點:
簡單來說,就是chind節點能複用就複用。不能複用,就重新繪製。
更新Widget
的邏輯如下:
\ | newWidget == null | newWidget != null |
---|---|---|
child == null | 返回null | 返回新的Element |
child != null | 移除舊的child並返回null | 如果舊child被更新就返回child,否則返回新的Element |
參考與致謝:
1.《Flutter核心技術與實戰》(陳航老師)
2. 小德 -- Flutter 技術分享
3.《Flutter從載入到顯示》 (聖文前輩)