Flutter 究竟是如何渲染的?

奇舞647發表於2020-04-13

之前,寫了一篇《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。

Flutter架構

那麼,這三層的職責是什麼?究竟分別做了哪些事呢?

從上到下開始,先從我們相對比較熟悉的 Framework 層說起。

1. Framework層:Dart實現的上層UI SDK

實現了:Animation(動畫)、Painting(圖形繪製)、Gestures(手勢操作)等功能,幷包裝成對應的 api 提供給上層開發者呼叫。
為了保證Flutter所繪製的控制元件與原生控制元件風格類似,Flutter封裝了Material(對應Android)、Cupertino(對應iOS)風格的UI元件庫,供開發者直接使用。

2. Engine層:Skia渲染 + DartVM 引擎

這層主要包含三塊:SkiaDartText
Skia 是渲染引擎,為 Framework 層提供“底層渲染”能力。
DartDart 執行時引擎,為 Framework 層提供執行時呼叫Dart和渲染能力。
Text 是文字排版,為 Framework 層提供檢視排版能力。

3. Embedder層:作業系統適配

對不同平臺作業系統的適配,包括一些配置:surface、執行緒、外掛等特性。
由於Flutter相關特性並不多,因此對不同平臺作業系統的適配成本很低。

二、Flutter 的 渲染策略

我們都知道,在Flutter中,Everything is widget

Flutter 究竟是如何渲染的?

所有 Widget 會組成 Widget Tree
介面更新時,會更新 Widget Tree
再更新 Element Tree ,最後更新 RenderObjectTree

那麼,究竟Flutter是怎麼更新介面的?又有哪些優化的渲染策略呢??

分為4個階段,分別是:

佈局階段 => 繪製階段 => 合成階段 => 渲染階段
Layout => Paint => Composite => Rasterize

1. 佈局(Layout)

Flutter採用 “深度優先” 機制遍歷Widget Tree。

我們都知道,在佈局過程中,Widget樹中的每個孩子節點都會受到父節點所加的位置約束。(也就是說,父節點決定孩子節點的位置,孩子節點只能決定自己的大小。)

先確定父節點的大小、位置,
再確定孩子節點的位置,
再確定孩子節點的大小。
....一直往下,
直到沒有孩子Widget後,返回完成。

Layout

PS:深度優先於廣度優先的區別:參考部落格

為了防止孩子節點的變化,導致整個 Widget Tree 重新佈局。
Flutter加入了 “佈局邊界” 機制(Relayout Boundary)。(劃重點,★優化點★)

在某些節點,“自動”或“手動”加上 “佈局邊界” ,控制邊界。
在該佈局邊界內的任何節點發生重新佈局,都不會影響邊界外的 Widget Tree的佈局。

Relayout Boundary

佈局完成後,樹上每個節點都確定了“尺寸大小”和“位置”。

2. 繪製(Paint)

剛才,我們確定了樹上的控制元件的“尺寸”與“位置”。
接下來是繪製階段。

和佈局類似,Flutter也是採用 “深度優先” 機制遍歷渲染樹。
先繪製自身,再繪製子節點。

Paint

但是,會出現一些繪製圖層覆蓋問題。
比如,當節點2需要重繪時,節點2、5、6一起重繪了(其實只要重繪節點2、5)。

Repaint Boundary

為了解決繪製覆蓋問題,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才不會感到卡頓。

Flutter渲染流水線

在流水線的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從載入到顯示》 (聖文前輩)

相關文章