前言:
最近,研究了一下GPU以及App的渲染流程與原理。
首先,感謝 QiShare團隊 的指導與支援,以及 鵬哥(@snow) 對本文的稽核與幫助。
接下來,讓我們開始我們今天的探索之旅。
一、淺談GPU
GPU(
Graphics Processing Unit
):
又名圖形處理器,是顯示卡的 “核心”。
主要負責影像運算工作,具有高並行能力,通過計算將影像顯示在螢幕畫素中。
GPU的工作原理,簡單來說就是:
—— 將 “3D座標” 轉換成 “2D座標” ,再將 “2D座標” 轉換為 “實際有顏色的畫素” 。
那麼,GPU具體的工作流水線 會分為“六個階段”,分別是:
頂點著色器
=>
形狀裝配=>
幾何著色器=>
光柵化=>
片段著色器=>
測試與混合
- 第一階段:頂點著色器(Vertex Shader)
該階段輸入的是頂點資料(Vertex Data
),頂點資料是一系列頂點的集合。頂點著色器主要的目的是把 3D 座標轉為 “2D” 座標,同時頂點著色器可以對頂點屬性進行一些基本處理。
( 一句話簡單說,確定形狀的點。)
- 第二階段:形狀裝配(Shape Assembly)
該階段將頂點著色器輸出的所有頂點作為輸入,並將所有的點裝配成指定圖元的形狀。
圖元(Primitive
) 用於表示如何渲染頂點資料,如:點、線、三角形。
這個階段也叫圖元裝配。
( 一句話簡單說,確定形狀的線。)
- 第三階段:幾何著色器(Geometry Shader)
該階段把圖元形式的一系列定點的集合作為輸入,通過生產新的頂點,構造出全新的(或者其他的)圖元,來生成幾何形狀。 ( 一句話簡單說,確定三角形的個數,使之變成幾何圖形。)
- 第四階段:光柵化(Rasterization)
該階段會把圖元對映為最終螢幕上相應的畫素,生成片段。片段(Fragment
) 是渲染一個畫素所需要的所有資料。
( 一句話簡單說,將圖轉化為一個個實際螢幕畫素。)
- 第五階段:片段著色器(Fragment Shader)
該階段首先會對輸入的片段進行裁切(Clipping
)。裁切會丟棄超出檢視以外的所有畫素,用來提升執行效率。並對片段(Fragment
)進行著色。
( 一句話簡單說,對螢幕畫素點著色。)
- 第六階段:測試與混合(Tests and Blending)
該階段會檢測片段的對應的深度值(z 座標),來判斷這個畫素位於其它圖層畫素的前面還是後面,決定是否應該丟棄。此外,該階段還會檢查 alpha
值( alpha
值定義了一個畫素的透明度),從而對圖層進行混合。
( 一句話簡單說,檢查圖層深度和透明度,並進行圖層混合。)
(PS:這個很關鍵,會在之後推出的“App效能優化實戰”系列部落格中,是我會提到優化UI效能的一個點。)
因此,即使在片段著色器中計算出來了一個畫素輸出的顏色,在經歷測試與混合圖層之後,最後的畫素顏色也可能完全不同。
關於混合,GPU採用如下公式進行計算,並得出最後的實際畫素顏色。
R = S + D * (1 - Sa)
含義:
R:Result,最終畫素顏色。
S:Source,來源畫素(上面的圖層畫素)。
D:Destination,目標畫素(下面的圖層畫素)。
a:alpha,透明度。
結果 = S(上)的顏色 + D(下)的顏色 * (1 - S(上)的透明度)
GPU渲染流水線的完整過程,如下圖所示:
問:CPU vs. GPU?
這裡引用我們團長(@月影)之前分享的一頁PPT:
由於螢幕每個畫素點有每一幀的重新整理需求,所以對GPU的並行工作效率要求更高。
簡單說完了GPU渲染的流水線,我們來聊一聊App的渲染流程與原理。
iOS App的渲染主要分為以下三種:
- 原生渲染
- 大前端渲染(
WebView
、類React Native
) - Flutter渲染
二、原生渲染
說到原生渲染,首先想到的就是我們最熟悉使用的iOS渲染框架:UIKit
、SwiftUI
、Core Animation
、Core Graphics
、Core Image
、OpenGL ES、Metal
。
UIKit
:日常開發最常用的UI框架,可以通過設定UIKit
元件的佈局以及相關屬性來繪製介面。其實本身UIView
並不擁有螢幕成像的能力,而是View
上的CALayer
屬性擁有展示能力。(UIView
繼承自UIResponder
,其主要負責使用者操作的事件響應,iOS事件響應傳遞就是經過檢視樹遍歷實現的。)SwiftUI
:蘋果在WWDC-2019
推出的一款全新的“宣告式UI”框架,使用Swift
編寫。一套程式碼,即可完成iOS
、iPadOS
、macOS
、watchOS
的開發與適配。(關於SwiftUI
,我去年寫過一篇簡單的Demo
,可供參考:《用SwiftUI寫一個簡單頁面》)Core Animation
:核心動畫,一個複合引擎。儘可能快速的組合螢幕上不同的可視內容。分解成獨立的圖層(CALayer
),儲存在圖層樹中。Core Graphics
:基於Quartz
高階繪圖引擎,主要用於執行時繪製影像。Core Image
:執行前影像繪製,對已存在的影像進行高效處理。OpenGL ES
:OpenGL for Embedded Systems
,簡稱GLES
,是OpenGL
的子集。由GPU
廠商定製實現,可通過C/C++
程式設計操控GPU
。Metal
:由蘋果公司實現,WWDC-2018
已經推出Metal2
,渲染效能比OpenGL ES
高。為了解決OpenGL ES
不能充分發揮蘋果晶片優勢的問題。
那麼,iOS原生渲染的流程有那幾部分組成呢?
主要分為以下四步:
-
第一步:更新檢視樹、圖層樹。(分別對應
View
的層級結構、View
上的Layer
層級結構) -
第二步:CPU開始計算下一幀要顯示的內容(包括檢視建立、佈局計算、檢視繪製、影像解碼)。當
runloop
在kCFRunLoopBeforeWaiting
和kCFRunLoopExit
狀態時,會通知註冊的監聽,然後對圖層打包,打完包後,將打包資料傳送給一個獨立負責渲染的程式Render Server
。 前面 CPU 所處理的這些事情統稱為Commit Transaction
。
- 第三步:資料到達
Render Server
後會被反序列化,得到圖層樹,按照圖層樹的圖層順序、RGBA
值、圖層frame
來過濾圖層中被遮擋的部分,過濾後將圖層樹轉成渲染樹,渲染樹的資訊會轉給OpenGL ES
/Metal
。
- 第四步:
Render Server
會呼叫GPU
,GPU
開始進行前面提到的頂點著色器、形狀裝配、幾何著色器、光柵化、片段著色器、測試與混合六個階段。完成這六個階段的工作後,就會將CPU
和GPU
計算後的資料顯示在螢幕的每個畫素點上。
那麼,關於iOS原生渲染的整體流程,我也畫了一張圖:
三、大前端渲染
1. WebView:
對於WebView
渲染,其主要工作在WebKit
中完成。
WebKit
本身的渲染基於macOS
的Lay Rendering
架構,iOS
本身渲染也是基於這套架構。
因此,本身從渲染的實現方式來說,效能應該和原生差別不大。
但為什麼我們能明顯感覺到使用WebView
渲染要比原生渲染的慢呢?
-
第一,首次載入。會額外多出網路請求和指令碼解析工作。 即使是本地網頁載入,
WebView
也要比原生多出指令碼解析的工作。WebView
要額外解析HTML+CSS+JavaScript
程式碼。 -
第二,語言解釋執行效能來看。JS的語言解析執行效能要比原生弱。 特別是遇到複雜的邏輯與大量的計算時,
WebView
的解釋執行效能要比原生慢不少。 -
第三,
WebView
的渲染程式是獨立的,每一幀的更新都要通過IPC
呼叫GPU
程式,會造成頻繁的IPC
程式通訊,從而造成效能消耗。並且,兩個程式無法共享紋理資源,GPU
無法直接使用context
光柵化,而必須要等待WebView
通過IPC
把context
傳給GPU
再光柵化。因此GPU
自身的效能發揮也會受影響。
因此,WebView
的渲染效率,是弱於原生渲染的。
2. 類React Native(使用JavaScriptCore
引擎做為虛擬機器方案)
代表:React Native
、Weex
、小程式等。
我們以 ReactNative
舉例:
React Native
的渲染層直接走的是iOS原生渲染,只不過是多了Json
+JavaScript
指令碼解析工作。
通過JavaScriptCore
引擎將“JS”與“原生控制元件”產生相對應的關聯。
進而,達成通過JS
來操控iOS
原生控制元件的目標。
(簡單來說,這個json
就是一個指令碼語言到本地語言的對映表,KEY
是指令碼語言認識的符號,VALUE
是本地語言認識的符號。)
簡單介紹一下,
JavaScriptCore
:
JavaScriptCore
是iOS
原生與JS
之間的橋樑,其原本是WebKit
中解釋執行JavaScript
程式碼的引擎。目前,蘋果公司有JavaScriptCore
引擎,谷歌有V8
引擎。
但與 WebView
一樣,RN也需要面臨JS語言解釋效能的問題。
因此,從渲染效率角度來說,WebView < 類ReactNative < 原生。
(因為json
的複雜度比html
+css
低)
四、Flutter渲染
首先,推薦YouTube上的一個視訊:《Flutter's Rendering Pipeline》專門講Flutter
渲染相關的知識。
1. Flutter的架構:
可以看到,Flutter
重寫了UI
框架,從UI
控制元件到渲染全部自己重新實現了。
不依賴 iOS
、Android
平臺的原生控制元件,
依賴Engine(C++)
層的Skia
圖形庫與系統圖形繪製相關介面。
因此,在不同的平臺上有了相同的體驗。
2. Flutter的渲染流程:
簡單來說,Flutter的介面由Widget
組成。
所有Widget
會組成Widget Tree
。
介面更新時,會更新Widget Tree
,
再更新Element Tree
,最後更新RenderObjectTree
。
更新Widget
的邏輯如下:
\ | newWidget == null | newWidget != null |
---|---|---|
child == null | 返回null | 返回新的Element |
child != null | 移除舊的child並返回null | 如果舊child被更新就返回child,否則返回新的Element |
接下來的渲染流程,
Flutter
渲染在 Framework
層會有 Build
、Widget Tree
、Element Tree
、RenderObject Tree
、Layout
、Paint
、Composited Layer
等幾個階段。
在 Flutter
的 C++
層,使用 Skia
庫,將 Layer
進行組合,生成紋理,使用 OpenGL
的介面向 GPU
提交渲染內容進行光柵化與合成。
提交到 GPU
程式後,合成計算,顯示螢幕的過程和 iOS
原生渲染基本是類似的,因此效能上是差不多的。
五、總結對比
渲染方式 | 語言 | 效能 | 對應群體 |
---|---|---|---|
原生 | Objective-C、Swift | ★★★ | iOS開發者 |
WebView | HTML、CSS、JavaScript | ★ | 前端開發者 |
類React Native | JavaScript | ★★ | 前端開發者 |
Flutter | Dart | ★★★ | Dart開發者 |
但Flutter的優勢在於:
- 跨平臺,可以同時執行在
iOS
、Android
兩個平臺。 - 熱過載(
Hot Reload
),省去了重新編譯程式碼的時間,極大的提高了開發效率。 - 以及未來谷歌新系統
“Fuchsia”
的釋出與加持。如果谷歌未來的新系統Fuchsia
能應用到移動端,並且領域替代Android
。由於Fuchsia
的上層是Flutter
編寫的,因此Flutter
開發成為了移動端領域的必選項。同時Flutter
又支援跨平臺開發,那麼其他領域的技術棧存在的價值會越來越低。
當然,蘋果的希望在於 SwiftUI
。
如果 Fuchisa
最終失敗了,SwiftUI
也支援跨端了。同時,SwiftUI
本身也支援熱過載。也許也是一個未來呢。
期待,蘋果今年6月的線上WWDC-2020
吧,希望能給我們帶來不一樣的驚喜。
參考與致謝:
1.《iOS開發高手課》(戴銘老師)
2.《你不知道的GPU》(月影)
3.《Flutter從載入到顯示》 (聖文前輩)
4.《UIKit效能調優實戰講解》(bestswifter)
5.《iOS - 渲染原理》
6.《iOS 影像渲染原理》
7.《計算機那些事(8)——圖形影像渲染原理》
8.《WWDC14:Advanced Graphics and Animations for iOS Apps》