iOS 淺談GPU及“App渲染流程”

奇舞647發表於2020-03-29

前言:
最近,研究了一下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)

該階段把圖元形式的一系列定點的集合作為輸入,通過生產新的頂點,構造出全新的(或者其他的)圖元,來生成幾何形狀。 ( 一句話簡單說,確定三角形的個數,使之變成幾何圖形。

GPU並行處理

該階段會把圖元對映為最終螢幕上相應的畫素,生成片段。片段(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渲染流水線的完整過程,如下圖所示:

GPU圖形渲染流水線

問:CPU vs. GPU?

這裡引用我們團長(@月影)之前分享的一頁PPT:

CPU vs. GPU

由於螢幕每個畫素點有每一幀的重新整理需求,所以對GPU的並行工作效率要求更高。


簡單說完了GPU渲染的流水線,我們來聊一聊App的渲染流程與原理。
iOS App的渲染主要分為以下三種:

  • 原生渲染
  • 大前端渲染(WebView、類React Native
  • Flutter渲染

二、原生渲染

說到原生渲染,首先想到的就是我們最熟悉使用的iOS渲染框架:UIKitSwiftUICore AnimationCore GraphicsCore ImageOpenGL ES、Metal

  • UIKit:日常開發最常用的UI框架,可以通過設定UIKit元件的佈局以及相關屬性來繪製介面。其實本身UIView並不擁有螢幕成像的能力,而是View上的CALayer屬性擁有展示能力。(UIView繼承自UIResponder,其主要負責使用者操作的事件響應,iOS事件響應傳遞就是經過檢視樹遍歷實現的。)
  • SwiftUI:蘋果在WWDC-2019推出的一款全新的“宣告式UI”框架,使用Swift編寫。一套程式碼,即可完成iOSiPadOSmacOSwatchOS的開發與適配。(關於SwiftUI,我去年寫過一篇簡單的Demo,可供參考:《用SwiftUI寫一個簡單頁面》
  • Core Animation:核心動畫,一個複合引擎。儘可能快速的組合螢幕上不同的可視內容。分解成獨立的圖層(CALayer),儲存在圖層樹中。
  • Core Graphics:基於 Quartz 高階繪圖引擎,主要用於執行時繪製影像。
  • Core Image:執行前影像繪製,對已存在的影像進行高效處理。
  • OpenGL ESOpenGL for Embedded Systems,簡稱 GLES,是 OpenGL 的子集。由GPU廠商定製實現,可通過C/C++程式設計操控GPU
  • Metal:由蘋果公司實現,WWDC-2018已經推出Metal2,渲染效能比OpenGL ES高。為了解決OpenGL ES不能充分發揮蘋果晶片優勢的問題。

那麼,iOS原生渲染的流程有那幾部分組成呢?
主要分為以下四步:

  • 第一步:更新檢視樹、圖層樹。(分別對應View的層級結構、View上的Layer層級結構)

  • 第二步:CPU開始計算下一幀要顯示的內容(包括檢視建立、佈局計算、檢視繪製、影像解碼)。當 runloopkCFRunLoopBeforeWaitingkCFRunLoopExit 狀態時,會通知註冊的監聽,然後對圖層打包,打完包後,將打包資料傳送給一個獨立負責渲染的程式 Render Server。 前面 CPU 所處理的這些事情統稱為 Commit Transaction

commit-transaction

  • 第三步:資料到達 Render Server 後會被反序列化,得到圖層樹,按照圖層樹的圖層順序、RGBA 值、圖層 frame 來過濾圖層中被遮擋的部分,過濾後將圖層樹轉成渲染樹,渲染樹的資訊會轉給 OpenGL ES/Metal

WWDC14-Session-419

  • 第四步:Render Server 會呼叫 GPUGPU 開始進行前面提到的頂點著色器、形狀裝配、幾何著色器、光柵化、片段著色器、測試與混合六個階段。完成這六個階段的工作後,就會將 CPUGPU 計算後的資料顯示在螢幕的每個畫素點上。

WWDC14-實際並行時圖解

那麼,關於iOS原生渲染的整體流程,我也畫了一張圖:

iOS-原生渲染流程


三、大前端渲染

1. WebView:

對於WebView渲染,其主要工作在WebKit中完成。
WebKit本身的渲染基於macOSLay Rendering架構,iOS本身渲染也是基於這套架構。
因此,本身從渲染的實現方式來說,效能應該和原生差別不大。

但為什麼我們能明顯感覺到使用WebView渲染要比原生渲染的慢呢?

  • 第一,首次載入。會額外多出網路請求和指令碼解析工作。 即使是本地網頁載入,WebView也要比原生多出指令碼解析的工作。 WebView要額外解析HTML+CSS+JavaScript程式碼。

  • 第二,語言解釋執行效能來看。JS的語言解析執行效能要比原生弱。 特別是遇到複雜的邏輯與大量的計算時,WebView 的解釋執行效能要比原生慢不少。

  • 第三,WebView的渲染程式是獨立的,每一幀的更新都要通過IPC呼叫GPU程式,會造成頻繁的IPC程式通訊,從而造成效能消耗。並且,兩個程式無法共享紋理資源,GPU無法直接使用context光柵化,而必須要等待WebView通過IPCcontext傳給GPU再光柵化。因此GPU自身的效能發揮也會受影響。

因此,WebView的渲染效率,是弱於原生渲染的。

2. 類React Native(使用JavaScriptCore引擎做為虛擬機器方案)

代表:React NativeWeex、小程式等。

我們以 ReactNative 舉例:
React Native的渲染層直接走的是iOS原生渲染,只不過是多了Json+JavaScript指令碼解析工作。
通過JavaScriptCore引擎將“JS”與“原生控制元件”產生相對應的關聯。
進而,達成通過JS來操控iOS原生控制元件的目標。 (簡單來說,這個json就是一個指令碼語言到本地語言的對映表,KEY是指令碼語言認識的符號,VALUE是本地語言認識的符號。)

簡單介紹一下,JavaScriptCore
JavaScriptCoreiOS 原生與 JS 之間的橋樑,其原本是 WebKit 中解釋執行 JavaScript 程式碼的引擎。目前,蘋果公司有 JavaScriptCore 引擎,谷歌有V8引擎。

但與 WebView 一樣,RN也需要面臨JS語言解釋效能的問題。

因此,從渲染效率角度來說,WebView < 類ReactNative < 原生。 (因為json的複雜度比html+css低)


四、Flutter渲染

首先,推薦YouTube上的一個視訊:《Flutter's Rendering Pipeline》專門講Flutter渲染相關的知識。

1. Flutter的架構:

Flutter架構

可以看到,Flutter重寫了UI框架,從UI控制元件到渲染全部自己重新實現了。
不依賴 iOSAndroid 平臺的原生控制元件,
依賴Engine(C++)層的Skia圖形庫與系統圖形繪製相關介面。
因此,在不同的平臺上有了相同的體驗。

2. Flutter的渲染流程:

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

image

接下來的渲染流程,
Flutter 渲染在 Framework 層會有 BuildWidget TreeElement TreeRenderObject TreeLayoutPaintComposited Layer 等幾個階段。
FlutterC++ 層,使用 Skia 庫,將 Layer 進行組合,生成紋理,使用 OpenGL 的介面向 GPU 提交渲染內容進行光柵化與合成。
提交到 GPU 程式後,合成計算,顯示螢幕的過程和 iOS 原生渲染基本是類似的,因此效能上是差不多的。

五、總結對比

渲染方式 語言 效能 對應群體
原生 Objective-C、Swift ★★★ iOS開發者
WebView HTML、CSS、JavaScript 前端開發者
類React Native JavaScript ★★ 前端開發者
Flutter Dart ★★★ Dart開發者

但Flutter的優勢在於:

  1. 跨平臺,可以同時執行在 iOSAndroid 兩個平臺。
  2. 熱過載(Hot Reload),省去了重新編譯程式碼的時間,極大的提高了開發效率。
  3. 以及未來谷歌新系統 “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》

相關文章