Flutter UI系統

位元組跳動ADFE團隊發表於2021-08-11

一、螢幕和影像

在介紹 Flutter 的 UI 系統之前,我們先來了解下螢幕是如何顯示圖形的,這有助於理解 UI 系統的設計。

1.1 基本單位 - 畫素

視覺化的所有的一切在本質上都是依靠繪製的,而繪製的基本單位則由螢幕中一個個物理顯示單位組成,每一個單位我們可以稱之為一個物理畫素點,而每一個畫素點可以發出多種顏色,顯示器成像的原理就是在不同的物理畫素點上顯示不同的顏色,最終構成完整的影像。

(圖來源於www.bilibili.com/video/BV1dA…視訊中22s截圖)

  • RGB 排列:每個畫素都是由3個緊密相鄰的RGB(紅綠藍的英文單詞首字母)子畫素整齊排列組成,這種排列方式常見於 LCD 螢幕。
  • Delta 排列:相比標準 RGB 排列,三種顏色的子畫素數量各減少三分之一,每個畫素呈 R-G、G-B 或 B-R 排列,六個畫素共用周圍一個子畫素,實際畫素密度大約只有標準 RGB 排列的 70%。
  • 鑽石 Pentile 排列:Pentile 排列每一行的子畫素數量都少了三個,最終總的子畫素數量則減少1/3。
  • 京東方排列:屬於 Delta 的一個變種,設計方案出自京東方。把一個綠色畫素拆成兩個子畫素。其畫素點排列方式看起來就像紅綠藍小人,小眼睛厚嘴脣,就像周冬雨的表情,又稱為周冬雨排列。

(圖來源於網路:wantubizhi.com/image.aspx?…)

上面介紹的佈局方案 RGB 排列常見於 LCD 螢幕,其他都是為了 OLED 螢幕而誕生的,因為 OLED 是自發光,屬於有機電致發光,而有機物容易失效且會自分解,失效的有機物便不能再發光。所以為了延長有機物的壽命誕生了各種排列布局方案增長 OLED 螢幕的壽命。

1.2 螢幕解析度

一個畫素點可以顯示出 1600 萬種顏色,由RGB三基色組成的螢幕,每個基本色(R、G、B)深度擴充套件至8 bit(位),即 2 的 24 次方為 1600 萬色,顏色深度越深,所能顯示的色彩更加豐富靚麗。那麼畫素點有多大呢?主要取決於顯示器的解析度,相同面積不同解析度的螢幕,其畫素點大小就不相同。

(圖來源於www.dazhuanlan.com/2019/10/12/…)

1.3 DPI

DPI 也常被稱為 PPI (每英寸畫素數),它是通過將螢幕的橫向或縱向畫素數除以以英寸為單位的寬度或高度來計算的。更高的 DPI 意味著每個畫素必須更小,以便能夠被可用空間容納,這意味著螢幕會更清晰,螢幕可以繪製的細節級別更高。

(圖來源於www.dazhuanlan.com/2019/10/12/…)

(圖來源於www.dazhuanlan.com/2019/10/12/…)

1.4 顯示原理

為了更新顯示畫面,螢幕是以固定的頻率重新整理(從 GPU 取資料),比如一部手機螢幕的重新整理頻率是 60Hz。當一幀影像繪製完畢後準備繪製下一幀時,顯示器會發出一個垂直同步訊號(如 VSync), 60Hz的螢幕就會一秒內發出 60 次這樣的訊號。而這個訊號主要是用於同步 CPU、GPU 和顯示器的。一般地來說,計算機系統中,CPU、GPU 和顯示器以一種特定的方式協作:CPU 將計算好的顯示內容提交給 GPU,GPU 渲染後放入幀緩衝區,然後視訊控制器按照同步訊號從幀緩衝區取幀資料傳遞給顯示器顯示。

(圖來源於www.jianshu.com/p/7b7975158…)

由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時螢幕會保留之前的內容不變。這就是介面卡頓的原因。

CPU 和 GPU 的任務是各有偏重的,CPU 主要用於基本數學和邏輯計算,而 GPU 主要執行和圖形處理相關的複雜的數學,如矩陣變化和幾何計算,GPU 的主要作用就是確定最終輸送給顯示器的各個畫素點的色值。

1.5 UI系統

由於最終的圖形計算和繪製都是由相應的硬體來完成,而直接操作硬體的指令通常都會有作業系統遮蔽,開發者通常不會直接面對硬體,通常硬體編碼開發效率低,作業系統遮蔽了這些底層硬體操作後會提供一些封裝後的API供作業系統之上的應用呼叫。

對於應用開發者來說,直接呼叫作業系統提供的API是複雜低效的,因為作業系統提供的API往往比較基礎,直接呼叫需要了解API的很多細節。所以開發GUI程式的程式語言都會在作業系統之上再封裝一層,將作業系統原生API封裝在一個程式設計框架和模型中,然後定義一種簡單的開發規則來開發GUI應用程式,而這一層抽象,正是“UI”系統,如Android SDK正是封裝了Android作業系統API,提供了一個“UI描述檔案XML+Java操作DOM”的UI系統,而iOS的UIKit 對View的抽象也是一樣的,他們都將作業系統API抽象成一個基礎物件(如用於2D圖形繪製的Canvas),然後再定義一套規則來描述UI,如UI樹結構,UI操作的單執行緒原則等。

而Flutter的原理正是如此,它提供了一套Dart API,然後在底層通過 skia 這種跨平臺的繪製庫(內部會呼叫作業系統API)實現了一套程式碼跨多端。

二、Flutter UI系統

Flutter 自帶渲染引擎,提供了一整套從底層渲染邏輯到上層開發語言的完整解決方案:檢視渲染完全閉環在其框架內部,不依賴於底層作業系統提供的任何元件,從根本上保證了檢視渲染在 Android 和 iOS 上的高度一致性。

那麼 Flutter 是怎麼實現這套解決方案的呢?我們先來看看 Flutter 的框架設計。

2.1 Flutter框架設計

(圖來源於flutter.cn/docs/resour…)

Flutter 採用 Embedder(作業系統適配層)、Engine(渲染引擎及 Dart VM 層)和 Framework(UI SDK 層)整體三層的劃分。上圖可以看出,Flutter 框架每一層的元件定義都有著明確的邊界,其向上提供的功能和向下依賴的能力也非常明確。

2.2 Embedder - 作業系統適配層

Embedder 是作業系統適配層,實現了渲染 Surface 設定,執行緒設定,以及平臺外掛等平臺相關特性的適配。從這裡我們可以看到,Flutter 平臺相關特性並不多,這就使得從框架層面保持跨端一致性的成本相對較低。

(圖來源於flutter.cn/docs/resour…)

2.3 Engine - 渲染引擎及 Dart VM 層

Engine 層主要包含 Skia、Dart 和 Text,實現了 Flutter 的渲染引擎、文字排版、事件處理和 Dart 執行時等功能。Skia 和 Text 為上層介面提供了呼叫底層渲染和排版的能力,Dart 則為 Flutter 提供了執行時呼叫 Dart 和渲染引擎的能力。而 Engine 層的作用,則是將它們組合起來,從它們生成的資料中實現檢視渲染。

(圖來源於flutter.cn/docs/resour…)

2.4 Framework - UI SDK 層

Framework 層則是一個用 Dart 實現的 UI SDK,包含了動畫、圖形繪製和手勢識別等功能。為了在繪製控制元件等固定樣式的圖形時提供更直觀、更方便的介面,Flutter 還基於這些基礎能力,根據 Material 和 Cupertino 兩種視覺設計風格封裝了一套 UI 元件庫。我們在開發 Flutter 的時候,可以直接使用這些元件庫。

(圖來源於flutter.cn/docs/resour…)

2.5 繪製流程

在瞭解 Flutter 框架設計之後,我們再來看看其實現原理,從 Flutter 官網上可以發現有原理介紹圖。

(圖來源於flutter.cn/docs/resour…)

通過原理圖拆解我們可以瞭解到,GPU 通過 VSync 訊號同步 UI執行緒,UI執行緒使用 Dart 來構建抽象的檢視結構,然後提交給 Skia 引擎渲染為 GPU 所需要的資料,通過 OpenGL 或者 Vulkan 提供給 GPU,在 GPU 執行緒進行圖層合成。

我們還可以根據 VSync 訊號來整理一下流程。

(圖來源於www.debugger.wiki/article/htm…)

Flutter 通過在兩個硬體時鐘的 VSync 訊號之間計算併合成檢視資料,Skia 交給 GPU 渲染:UI 執行緒使用 Dart 來構建檢視結構資料,在 GPU 執行緒進行圖層合成,隨後交給 Skia 引擎加工成 GPU 資料,通過 OpenGL 最終提供給 GPU 渲染。

Flutter 的整個繪製流程是基於 Dart API 也是呼叫作業系統API,所以客戶端僅提供一塊畫布即可獲得從業務邏輯到功能呈現的多端高度一致的渲染體驗且效能接近原生。

三、Flutter UI 系統的基石

在瞭解 Flutter UI系統和作業系統互動的這一部分原理之後,再來看看 Flutter 是如何通過 Dart API 來操控畫布的。

3.1 頁面結構

通常我們在一個畫布上畫畫時,都需要對畫作有一個大致的輪廓的設計即線稿,將畫布分為一塊塊區域,而Flutter 開發時也是一樣。

通過上圖 Flutter 示例頁面的結構中可以看到頁面樹結構是由一個個控制元件所形成的。然而在 Flutter 體系結構中,真正做元件渲染在螢幕上這個任務的並非在 控制元件層(Widget)層,而是在渲染(Rendering)層。渲染層有著極為重要的Element 樹和 RenderingObject 樹兩棵樹。

3.2 三棵樹

(圖來源於book.flutterchina.club/chapter14/e…)

Flutter 頁面中各介面元素(Widget)以樹的形式組織,即控制元件樹。Flutter 通過控制元件樹中的每個控制元件建立不同型別的渲染物件,組成渲染物件樹。但實際上Flutter中真正代表螢幕上顯示元素的是 Element ,Widget 只是描述 Element 的配置資料。所以UI樹其實是由一個個獨立的Element節點構成。

元件最終的 Layout、渲染都是通過 RenderObject 來完成的,從建立到渲染的大體流程是:根據Widget 生成 Element,然後建立相應的 RenderObject 並關聯到 Element.renderObject 屬性上,最後再通過 RenderObject 來完成佈局排列和繪製。

3.3 總結

Flutter 通過控制元件樹中的每個控制元件建立不同型別的渲染物件,組成渲染物件樹。然後 RenderObject 將所有的圖層根據大小、層級、透明度等規則計算出最終的顯示效果,將相同的圖層歸類合併,簡化渲染樹,提高渲染效率。合成完成後,Flutter 會將幾何圖層資料交由 Skia 引擎加工成二維影像資料,最終交由 GPU 進行渲染,完成介面的展示。

參考資料:

Flutter官網

Flutter實戰

螢幕排列講解

畫素,點和解析度

螢幕顯示影像原理

作者:林凱鋒

相關文章