前言:
最近,研究了一下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
原生渲染基本是類似的,因此性能上是差不多的。
更多細節,可以查看:《Flutter 究竟是如何渲染的?》
五、總結對比
渲染方式 | 語言 | 性能 | 對應群體 |
---|---|---|---|
原生 | 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》