GUI Framework Inside
閱前提醒:本文僅屬個人觀點,如有雷同純屬巧合,如有錯誤請指正。
GUI 差不多已經發展了近 30年,到現在這項技術已經基本成熟,各種 GUI框架基本已經大同小異,下面是流行的 GUI框架一覽:
- Cocoa/Cocoa Touch (Apple, macOS/iOS)
- Windows Presentation Foundation (Microsoft, Windows)
- Android GUI (Google, Android)
- WebKit (Apple, Safari)
- Blink (Google, Chrome)
- Flutter (Google, Android/iOS/Fuschia/Chrome)
電影與顯示器:
電影是近代最偉大的發明之一,它的原理是人眼的“視覺暫留”,一個物體的視相消失後可以在視網膜短暫停留 0.1-0.4s,當電影膠片以 24格每秒勻速轉動時,一系列靜態畫面就會因為視覺暫留而造成一種連續的視覺印象。
現代顯示器一般以 60Hz(>=60)的重新整理頻率為主,這就要求顯示處理器(GPU)在 1s內提供 60張可供顯示的圖片資料,當顯示處理器來不及處理這麼多隻能提供少於 60這個數量時,很多人就能感覺到卡頓(掉幀),當這個值少於 30甚至少於 24時,基本上所有人都能感受到卡頓(掉幀)。
GUI框架的一般形式
GUI是電影(動畫)的一種延伸,它以動畫的形式向用戶展示可互動介面,對使用者的操作進行視覺上的反饋。
從上面可以知道,GUI框架理所應當向 GPU在 1s內提供至少 60次靜態影象,GUI框架本身也需要在 1s內完成一定量的計算,以完成使用者的互動需求。
使用者操作的基本 GUI單位是控制元件,按鈕是控制元件,圖片是控制元件,甚至視窗也是控制元件。應用通過控制元件之間的排列與組合為使用者提供豐富多彩的可互動介面。
控制元件也是 GUI框架進行渲染的基本單位,GUI框架通過控制元件樹描述介面的資料結構,通過控制元件的樣式屬性來確定大小和外觀。
早期的 GUI框架通過設計出不同的控制元件後,讓控制元件自己決定繪製的樣式,這樣做雖然沒有什麼問題,但通常效率都不夠高。不同控制元件都會存在很多類似的繪製程式碼(程式碼冗餘),並且在真正繪製時讓 GPU疲於奔命,而且每個控制元件的繪製週期都比較獨立,很難從框架總體上去進行管控和定位問題。
經過多年的迭代和發展後,GUI框架發展成如今這種大同小異的結構:

- Widgets(控制元件,也叫 View Tree),它是用於描述使用者介面原始資料的樹狀結構。通常這一層根本不關心繪製,它只關心使用者對資料的操作。
- Render Tree,它是一種更為抽象的樹狀資料結構,一般來說它是和上一步的 View Tree結構相同,並且它不關心原始資料,只關心控制元件的佈局和大小。通過這一步計算出控制元件佈局後才能真正地確定控制元件的外觀。
- Layer Tree 跟 Render Tree是相對應的,這一步會主動觸發 Render Tree中每個元素的外觀渲染,在已知控制元件大小和位置的情況下決定每個控制元件的真正外觀。但 Layer Tree的樹狀結構不是和 Render Tree一一對應的,Layer Tree有可能因為 Layer合併優化導致一層的 Render Tree葉子節點最終只對應一個 Layer。
- 在已經決定好控制元件的大小位置以及長相後,剩下的工作就需要把這些東西組合起來顯示到螢幕上。這一步原理比較簡單,就是將前一步的 Layer合併成一張 Bitmap,這是一種最簡單的影象儲存形式。將 Bitmap光柵化後便可以提交給 GPU渲染。
渲染流程
從巨集觀上來總結大多數 GUI框架的渲染流程,除了部分框架在處理 Animation的時機有所不同,基本都可以總結為下圖:
(該圖選自 Flutter Rendering Pipeline技術專題演講)
Layout
Layout階段主要負責計算出檢視的大小和位置。
Webkit / Blink
Webkit 依靠 CSS(Cascading Style Sheet)實現佈局。
CSS有以下特點:
- 選擇器:確定樣式的作用元素
- 樣式:確定檢視的佈局、大小、背景等外觀。
- 變形、變換和動畫:用於相對複雜的動畫或外觀形式
看起來 CSS對於 Webkit而言不止只是一個佈局功能的實現,而且還是繪製以及動畫的實現。CSS為 Webkit提供了框模型佈局以及 Flex佈局,通過這兩個佈局的組合基本上可以實現任意形式的佈局需求。
Cocoa / Cocoa Touch
Cocoa 以及 Cocoa Touch早期一直依賴簡單的框(frame)模型佈局,直到 iOS裝置的形式開始豐富起來時,才把 AutoLayout這一佈局搬上歷史舞臺。
當然不乏有第三方框架將 FlexBox的佈局搬到 Cocoa Touch 上,但其依賴的根本還是框模型佈局,並沒有被 GUI框架內建參與到渲染流程中(這也無傷大雅)。
Android GUI
Android僅僅只支援了框模型佈局,不過很早 Google 工程師就認識到這是遠遠不夠的,所以 Android有著更多預定義的佈局模型(比如 LinearLayout、GridLayout)來幫助開發者實現更復雜的佈局。
Flutter
Flutter可以說和 Blink是同宗同源的,但是它的佈局模式卻集各家所長。 Flutter同時支援框佈局,Flex佈局,以及類似於 Android的預定義佈局模型。
但 Flutter和 Android GUI的佈局都有個不太明顯的缺點:佈局模型也成為了控制元件之一,這看起來有一些奇怪,很明顯佈局不能渲染內容。
Paint
paint 階段主要負責計算出檢視的內容。

一般來說,GUI框架在這個階段需要呼叫圖形相關的功能或函式來表達出每一層(Layer)的內容資料。如果繪圖操作由 CPU計算完成,那麼稱之為軟體繪圖。如果由 GPU完成,那麼稱之為硬體加速繪圖。通常這兩種繪圖 CPU居多,但混合的情況是經常有的。CPU只能處理 2D繪圖,當碰到 3D的情況時只能由 GPU完成這部分工作。
Cocoa / Cocoa Touch
macOS 一直以來都依靠 Quarz 2D (Core Graphics)來渲染檢視,直到 iOS上才使用了更高效能、更現代化設計的 Core Animation.
在渲染 3D時,可以使用 OpenGL ES,不過在 iOS 12上已經去除了支援,改為自家的 Metal引擎。
Android GUI、Flutter 和 Blink
三者都來自 Google,因此他們的 2D渲染引擎都採用了 Skia。
3D繪圖採用了流行的 OpenGL(ES)
WebKit
WebKit的繪圖實現就比較抽象了,從設計之初為了支援跨平臺,把各平臺有差異的繪圖介面抽象為統一介面:PlatformGraphicsContext. 這一個介面在 macOS/iOS 平臺的實現就是 CoreGraphics,在 Android的實現就是 Skia.
3D繪圖的道理也是一樣,WebKit抽象出了 PlatformGraphicsContext3D的介面。
Composite
通常在 Paint階段渲染的 Layer所使用的畫素(pixel)都遠遠超過了螢幕所能承載的,很明顯在顯示一屏內容時不需要這麼多畫素,GPU沒必要為額外那麼多沒用的畫素資料執行計算,所以需要 Composite這一步進行 Layer合成。
由於 Paint階段已經決定了每一個 Layer的外觀資料存在記憶體中,所以合成階段只需要從記憶體中取資料計算,決定某一塊具體顯示哪一個 Layer的資料。這一階段過後得到的將是一份向量圖資料,在進行光柵化後提交給 GPU執行渲染即可。
設計一個 GUI框架?
在做 GUI框架各部分的選型之前,先來看一下目前各種 GUI框架的比較
比較項 | Paint | Layout | SDK |
---|---|---|---|
React-Native | / | Frame+FlexBox | Javascript |
Flutter | Skia | Frame+FlexBox+LayoutWidgets | Dart |
Cocoa(Touch) | CoreAnimation | Frame+AutoLayout | objc/Swift |
Android GUI | Skia | LayoutWidgets+Frame | Java |
WebKit | Portable* | Frame+FlexBox | Javascript |
(從上表可以看出 React-Native不能算是一個 GUI框架,最多算是一個應用層的SDK,幫助你在GUI框架之上構建控制元件。)
Paint的選型
我們很多人都希望多個平臺的應用只需要寫一份程式碼,所以 Paint的選型尤為重要。目前只有 WebKit和 Flutter實現了跨平臺,前者通過抽象出平臺的繪圖介面,後者使用跨平臺的繪相簿,兩者均可以移植到不同的平臺。
不過非要作比較,我個人還是比較認可 WebKit的方案。WebKit理論上可以移植到任何系統,而 Flutter如果 Skia庫不支援的話就不能移植。並且 WebKit的方案可以減少應用的體積,Skia的二進位制檔案還是太大了。
這一輪 PK我覺得還是 Portable*的抽象繪圖介面勝出。
Layout的選型
Layout關係著開發者的體驗。事實已經證明 Frame + FlexBox是非常友好而且非常高效的佈局方式,就算面對極其複雜的佈局方式,也只需要框架層提供部分佈局控制元件給予輔助即可。
SDK的設計
在設計 SDK前,我們需要先確定使用什麼程式語言。我們希望跨平臺,動態化,入門門檻較低,那麼似乎只有 JavaScript和 Dart可選。但仔細一想發現 Dart的執行時還是太大了,而 JavaScript的執行時早已被各個平臺內建,我們只需要引用即可。
WebKit DOM API經過時間的證明已經是個沉重的歷史包袱。若不是這種介面構建方式,React、Vue之類的框架也不會如今這般火熱。說到底 GUI開發也是程式設計,僅僅描述介面是不夠的,所以 SDK應當是一個類似於 UIKit之類的程式設計框架。
最終,我心目中理想的 GUI框架可能是這樣的:
這樣設計可能符合我的幾點要求:
- 跨平臺,用 Portable的抽象圖形API實現繪圖,基本可以抹平平臺差異
- 輕量級,沒有額外圖形庫的引入,也沒有額外語言執行時的引入,除了資料結構和渲染邏輯,沒有額外的體積佔用
- 門檻低,用 Javascript作為 SDK實現,怕是沒有門檻再低的辦法了
- 動態化,JIT執行的語言寫業務可以動態化
- 歷史包袱小,GUI框架本身可以隨著應用升級而升級,有問題可以及時修復發版。可以說這樣設計其實就是一個沒有歷史包袱、嵌入式的、去掉了一大堆網路等模組只關心渲染的迷你 WebKit了。
The End
GUI Framework的設計其實一點兒也不簡單,View Tree、Layout、Paint任何一個階段都是一個非常大的課題。上文只是一種看法和暢想,要做一些嘗試還是太費時費力了,有這時間不如多玩幾把荒野大表哥。