iOS 底層渲染原理

OpenGL render theory on iOS

iOS 底層渲染原理

寫在前面

下半年做過一次分享會,是以板書的形式分享。當時留下了一些手稿,最近整理一下分享給更多的人。

iOS 系統是如何實現 AddSubView 的?

在 iOS 開發中絕大部分的 UI 操作都靠 UIKit 完成,蘋果已經讓 UIKit 足夠好用。

在你使用 UIKit 的 AddSubView 時,有沒有想過 iOS 是如何更改屏幕上的對應像素的?蘋果為什么不讓開發者在輔助線程使用UIKit?

本文將從 AddSubView 出發,帶你了解 iOS 底層是如何完成渲染的。

我用 OpenGL 來描述系統的渲染行為,雖然新的系統中 OpenGL 已經被 Metal 代替,但原理是類似的。

iOS 渲染框架

iOS 的渲染渲染框架有 UIKit,CoreAnimation/Quartz,OpenGL,Metal。

iOS 渲染框架

UIKit,CoreAnimation 可以看做是底層渲染技術的 shell 層,它們并不直接提交渲染命令給 GPU,而是做數據的組裝和傳遞。此外 OpenGL 在 A11 以后已經不再直接調用系統驅動,而是調用 Metal 完成渲染。

UIKit 已經幫你做了數據的抽象,UI 工作可以很簡單地完成。CoreAnimation 層能接觸到更多的數據,如動畫時間曲線,CAlayer 等。UIView 的操作單位是 Point,CALayer 的是 Pixels。

UIKit 不用關注 Pixels 的變化,而 Pixels 的操作流程正是整個渲染流程的關鍵,也是這次分享的主題。

硬件相關

為了更容易理解底層渲染技術,了解一些 GPU 的硬件知識是有必要的。

GPU-CPU硬件結構

GPU 與 CPU 的最大區別就是它是高度專用化的,它的目的就是更快地渲染。這個問題等效于更快計算出每個像素的值,所以 GPU 在硬件結構上做了對應的取舍。

GPU 大大弱化了邏輯控制的單元,把大部分芯片空間都讓給了算術邏輯單元(ALU),Excution context 管理計算的上下文。這樣 SIMD(單指令多數據)架構上保證了一批像素的同一個計算,只需要一次操作就可以完成,這也是數據并行加速的原理。

移動端 GPU

ARM 架構下,GPU 主要由 Imagination 和高通壟斷,其中 A11 之后 iOS 上 GPU 由蘋果自研。

CPU 和 GPU 之間靠總線傳遞數據,而移動端的總線帶寬相比 PC 低很多,所以經常出現 GPU 空等 CPU 的情況。

雖然 GPU 自身的處理能力很強,但帶寬限制了數據的吞吐量,所以壓縮紋理和 TBR/TBDR 的應運而生。

bandwidth瓶頸

需要關注什么?

除了壓縮紋理和 TBR/TBDR,各個廠家同樣在驅動層為了同樣的目的做了一些優化。對于 UI 操作需要關注的是:

  1. 渲染命令異步提交

你在 CPU 提交的渲染命令是異步提交給 GPU 的(前提是可以異步的命令,同步命令 CPU 要等 GPU 執行完)。具體流程是

CPU -> CPU command queue -> 系統調度 -> GPU command queue -> GPU

所以在更改 UI 的當前 RunLoop ,GPU 實際還沒有開始繪制,不要預期有同步的渲染結果。

因為是異步提交命令,所以OpenGL Crash的現場并不是真正的問題所在,而是之前的命令導致狀態機錯誤,引發了Crash。

  1. 多幀緩存

系統底層驅動一般至少會做兩幀的緩沖,保證當前的渲染命令不影響當前上屏。但一些驅動會做更多的緩沖,iPhone8 以上至少會做 3 份緩沖,而這些緩沖是拿不到的,所以理論上從你的 UI 操作提交到 GPU,到真正展示會隔一小段時間。所以不要試圖精準地拿到對應的 CALayer 數據。

  1. 每一幀清屏是個好習慣

現在大部分移動端GPU都是TBR/TBDR的,如果要保留上一幀渲染結果,會有額外的GPU內存讀取操作,每一幀都清空畫布會帶來更高的性能,這也是蘋果推薦用不透明視圖的原因(部分原因)。蘋果默認會這樣設置CALayer,這也是開啟多幀緩沖的先決條件。

render pipeline

渲染管線是一次 GPU 渲染數據的操作流程,這個過程中 OpenGL 完成了屏幕 坐標的確認和對應屏幕像素值得計算。

render pipeline

渲染管線可以分為兩部分:

1. geometry pipeline

這個過程確定了輸入的 3D 坐標在屏幕上對應的 2D 坐標。

對于 CPU 傳來的 3D 頂點數據,會經過 Vertex processing 和 vertex post processing 兩個過程,轉換成 NDC 坐標(標準化設備坐標),經過圖元組裝后,就可以知道當前圖形影響屏幕上哪些像素。

對于 AddSubView 來說,就是通過 subView 的 frame,確認了 subView 對應的像素。

下面講一下 3D 坐標轉 2D 坐標的過程,不感興趣的可以跳過。

geometry pipeline

geometry pipeline 可以分為 3 個部分:

1. vertex processing

頂點輸入后,要在 Vertex shader 中做一系列坐標變換,最終將 3D 坐標轉換到一個[-1, 1]的空間內。

之后有可選的 Tessllation 和 Geometry shader 階段,它們可以動態改變圖元:比如改變圖元的形狀,增加圖元等。(使用 Tesslation 或 Geometry shader 后,管線會提前一部分圖元裝配的工作,這樣就保證了它們可以操作圖元)。

這一部分在 OpenGL 中是可編程的,我們可以控制頂點數據如何變換。

2. vertex post processing

這一部分是不可編程的,渲染管線會自動調用。

經過前面的處理,頂點坐標被變換成各分量都在[-1,1]之間的裁剪坐標,不在此區間的坐標被丟棄。

接下來會做透視除法,將坐標轉換為 NDC 坐標,你可以理解為完成了近大遠小的縮放。

最后做 viewport 變換,將 NDC 坐標根據屏幕的像素大小(viewport 參數),映射成屏幕坐標。

變換管線

你可能注意到在 geometry pipeline 階段我說到了多種不同的坐標,這其實正是 OpenGL 確定 3D 坐標到 2D 坐標的過程,實際上每一種坐標和相應的變換都是固定的。

transform pipeline

在第一部分,CPU 傳給 GPU 的頂點坐標為局部坐標,所有位置都是相對于自身原點的。

之后通過 vertex shader 中的model transfrom, view transfrom, projection transfrom,變為裁剪坐標,這一部分是決定如何展示頂點的主要工作,。

然后通過 vertex post-processing 階段,做透視除法和viewport transform,最終得到屏幕坐標。

最終輸出的屏幕坐標,會作為下一步圖元裝配的輸入。

這一部分可能比較難理解,你可以認為這一階段是在 3D 世界中,如何用照相機照相。

圖元裝配

圖元裝配即完成用圖元來描述你要繪制的形狀。圖元一般是三角形,因為在 3D 中,三角形能唯一確定一個平面。

對 AddSubView 來說,subView 會被組裝成兩個三角形圖元。圖元裝配之后,就可以確定當前圖形的屏幕區域。

2. pixels pipeline

pixels pipeline 主要完成了計算每個像素值的工作。

主要是下面三步:

格柵化

圖元裝配確定了圖形的屏幕區域,格柵化是在硬件層面,確定當前圖形會影響屏幕上哪些像素。所有包含在圖形內的像素,作為下一階段的輸入。

fragment processing

格柵化之后,渲染管線會將這一批像素作為 fragment shader 的輸入。這一過程是可編程的。

在 fragment shader 中,需要計算出每個像素是什么值。對 AddSubView 來說,如果 subView 的背景是圖像,需要把圖像作為紋理傳到 GPU,在計算對應像素時去采樣紋理。

也就是說這一階段確定了 subView 的樣子。

test & blend

最后這個決定了像素是否應該被展示。

stencil test

如果開啟了 stencil test,那么像素對應的 stencil 值不為 0,像素才會被展示。

depth test

如果開啟了 depth test,像素必須沒有被遮擋(z 軸的前后關系),才會被展示。

blend

如果開啟了 blend,像素的會根據 blend function 和 alpha 值,確定最終的像素值。

對 AddSubView 來說,如果 subView 設置了 mask,那么就要設置 stencil 來剔除 mask 之外的像素。如果 subView 是半透明,那么要根據 alpha 值和背景的顏色做 blend 操作。

UIKit 如何實現 addSubView

現在,再來說說 UIKit 如何實現 addSubView 的。

首先計算出 subView 的 frame,獲取背景圖像并解碼成位圖,獲取 mask,alpha 等屬性。

  1. 創建 GLContext。

  2. 取得當前 CALayer,作為當前 GLContext FBO 的 RBO。

  3. 將 frame 的四個頂點傳給 vertex shader。將背景圖像的位圖傳給 fragment shader。

  4. 在 vertex shader 中,由于是純 2D 的界面,可以直接將頂點 z 軸坐標都設為 1。將坐標轉為相對于 window 的坐標(或者使用正視投影)。

  5. 在 fragment shader 中,每個 SubView 的像素,都從圖像中采樣,作為輸出的顏色值。

  6. 在最后的 test & blend 中,對 subView 的 alpha,mask 屬性做對應處理。

  7. 展示當前 GLContext RBO。

這個流程也是系統 UIKit -> CoreAnimation -> render server 的過程。

OpenGL 狀態機

你可以把 GLContext 理解為 OpenGL 的狀態機,但實際上它是狀態機的超集。

OpenGL 狀態機

GLContext 可以看做是一個巨大的結構體,它維護著當前的渲染狀態,包括 GLObject 也是一個狀態的子集。

每一條渲染命令,其實都是在更改當前的渲染狀態,從而最終影響渲染流程。
而蘋果會自動在主線程設置一個 GLContext,來實現 UI 的渲染。

最后

每個 iOS 開發者都知道不能在輔助線程做 UI 操作,絕大多數卻不知道為什么蘋果要做這個限制。

看完了這篇文章,你應該能明白為什么:多線程操作 UI,意味著多線程在一個GLContext上渲染,蘋果默認不會提交輔助線程的渲染命令到GPU(雖然蘋果現在用的是Metal,并且渲染是單獨一個進程,但原理類似,操作GPU的接口不是線程安全的)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉載自VR設計云課堂[http://www.lxweimin.com/u/c7ffdc4b379e]Unity S...
    水月凡閱讀 1,042評論 0 0
  • 渲染管線 蘋果提供了兩種OpenGL ES的可視化模型,一種是客戶端—服務端的架構模型,另一種就是管線的模型。 客...
    sellse閱讀 12,182評論 1 10
  • 本文首發于個人博客:Lam's Blog - 【OpenGL ES】入門及繪制一個三角形,文章由MarkDown語...
    格子林ll閱讀 7,332評論 2 18
  • 轉載注明出處:點擊打開鏈接 Shader(著色器)是一段能夠針對3D對象進行操作、并被GPU所執行的程序。Shad...
    游戲開發小Y閱讀 3,432評論 0 4
  • 1.對著鏡子里的自己說我愛你,無論你是怎樣的,無論你表現得好不好,無論你有多少缺點,我都要好好的愛你,突然很想哭,...
    妙知閱讀 389評論 0 3