iOS 渲染原理解析
作者丨Rickey
來源丨一瓜技術(shù)(tech_gua)

0. 本文知識目錄

1. 計算機渲染原理
CPU 與 GPU 的架構(gòu)
對于現(xiàn)代計算機系統(tǒng),簡單來說可以大概視作三層架構(gòu):硬件、操作系統(tǒng)與進程。對于移動端來說,進程就是 app,而 CPU 與 GPU 是硬件層面的重要組成部分。CPU 與 GPU 提供了計算能力,通過操作系統(tǒng)被 app 調(diào)用。

-
CPU(Central Processing Unit):現(xiàn)代計算機整個系統(tǒng)的運算核心、控制核心。 -
GPU(Graphics Processing Unit):可進行繪圖運算工作的專用微處理器,是連接計算機和顯示終端的紐帶。
CPU 和 GPU 其設(shè)計目標就是不同的,它們分別針對了兩種不同的應(yīng)用場景。CPU 是運算核心與控制核心,需要有很強的運算通用性,兼容各種數(shù)據(jù)類型,同時也需要能處理大量不同的跳轉(zhuǎn)、中斷等指令,因此 CPU 的內(nèi)部結(jié)構(gòu)更為復雜。而 GPU 則面對的是類型統(tǒng)一、更加單純的運算,也不需要處理復雜的指令,但也肩負著更大的運算任務(wù)。

而 GPU 擁有更多的計算單元 Arithmetic Logic Unit,具有更強的計算能力,同時也具有更多的控制單元。GPU 基于大吞吐量而設(shè)計,每一部分緩存都連接著一個流處理器(stream processor),更加適合大規(guī)模的并行計算。
圖像渲染流水線
圖像渲染流程粗粒度地大概分為下面這些步驟:

上述圖像渲染流水線中,除了第一部分 Application 階段,后續(xù)主要都由 GPU 負責,為了方便后文講解,先將 GPU 的渲染流程圖展示出來:

上圖就是一個三角形被渲染的過程中,GPU 所負責的渲染流水線。可以看到簡單的三角形繪制就需要大量的計算,如果再有更多更復雜的頂點、顏色、紋理信息(包括 3D 紋理),那么計算量是難以想象的。這也是為什么 GPU 更適合于渲染流程。
接下來,具體講解渲染流水線中各個部分的具體任務(wù):
Application 應(yīng)用處理階段:得到圖元
這個階段具體指的就是圖像在應(yīng)用中被處理的階段,此時還處于 CPU 負責的時期。在這個階段應(yīng)用可能會對圖像進行一系列的操作或者改變,最終將新的圖像信息傳給下一階段。這部分信息被叫做圖元(primitives),通常是三角形、線段、頂點等。
Geometry 幾何處理階段:處理圖元
進入這個階段之后,以及之后的階段,就都主要由 GPU 負責了。此時 GPU 可以拿到上一個階段傳遞下來的圖元信息,GPU 會對這部分圖元進行處理,之后輸出新的圖元。這一系列階段包括:
-
頂點著色器(Vertex Shader):這個階段中會將圖元中的頂點信息進行視角轉(zhuǎn)換、添加光照信息、增加紋理等操作。 -
形狀裝配(Shape Assembly):圖元中的三角形、線段、點分別對應(yīng)三個 Vertex、兩個 Vertex、一個 Vertex。這個階段會將 Vertex 連接成相對應(yīng)的形狀。 -
幾何著色器(Geometry Shader):額外添加額外的Vertex,將原始圖元轉(zhuǎn)換成新圖元,以構(gòu)建一個不一樣的模型。簡單來說就是基于通過三角形、線段和點構(gòu)建更復雜的幾何圖形。
Rasterization 光柵化階段:圖元轉(zhuǎn)換為像素
光柵化的主要目的是將幾何渲染之后的圖元信息,轉(zhuǎn)換為一系列的像素,以便后續(xù)顯示在屏幕上。這個階段中會根據(jù)圖元信息,計算出每個圖元所覆蓋的像素信息等,從而將像素劃分成不同的部分。

一種簡單的劃分就是根據(jù)中心點,如果像素的中心點在圖元內(nèi)部,那么這個像素就屬于這個圖元。如上圖所示,深藍色的線就是圖元信息所構(gòu)建出的三角形;而通過是否覆蓋中心點,可以遍歷出所有屬于該圖元的所有像素,即淺藍色部分。
Pixel 像素處理階段:處理像素,得到位圖
經(jīng)過上述光柵化階段,我們得到了圖元所對應(yīng)的像素,此時,我們需要給這些像素填充顏色和效果。所以最后這個階段就是給像素填充正確的內(nèi)容,最終顯示在屏幕上。這些經(jīng)過處理、蘊含大量信息的像素點集合,被稱作位圖(bitmap)。也就是說,Pixel 階段最終輸出的結(jié)果就是位圖,過程具體包含:
這些點可以進行不同的排列和染色以構(gòu)成圖樣。當放大位圖時,可以看見賴以構(gòu)成整個圖像的無數(shù)單個方塊。只要有足夠多的不同色彩的像素,就可以制作出色彩豐富的圖象,逼真地表現(xiàn)自然界的景象??s放和旋轉(zhuǎn)容易失真,同時文件容量較大。
-
片段著色器(Fragment Shader):也叫做 Pixel Shader,這個階段的目的是給每一個像素 Pixel 賦予正確的顏色。顏色的來源就是之前得到的頂點、紋理、光照等信息。由于需要處理紋理、光照等復雜信息,所以這通常是整個系統(tǒng)的性能瓶頸。 -
測試與混合(Tests and Blending):也叫做 Merging 階段,這個階段主要處理片段的前后位置以及透明度。這個階段會檢測各個著色片段的深度值 z 坐標,從而判斷片段的前后位置,以及是否應(yīng)該被舍棄。同時也會計算相應(yīng)的透明度 alpha 值,從而進行片段的混合,得到最終的顏色。
2. 屏幕成像與卡頓
在圖像渲染流程結(jié)束之后,接下來就需要將得到的像素信息顯示在物理屏幕上了。GPU 最后一步渲染結(jié)束之后像素信息,被存在幀緩沖器(Framebuffer)中,之后視頻控制器(Video Controller)會讀取幀緩沖器中的信息,經(jīng)過數(shù)模轉(zhuǎn)換傳遞給顯示器(Monitor),進行顯示。完整的流程如下圖所示:

經(jīng)過 GPU 處理之后的像素集合,也就是位圖,會被幀緩沖器緩存起來,供之后的顯示使用。顯示器的電子束會從屏幕的左上角開始逐行掃描,屏幕上的每個點的圖像信息都從幀緩沖器中的位圖進行讀取,在屏幕上對應(yīng)地顯示。掃描的流程如下圖所示:

電子束掃描的過程中,屏幕就能呈現(xiàn)出對應(yīng)的結(jié)果,每次整個屏幕被掃描完一次后,就相當于呈現(xiàn)了一幀完整的圖像。屏幕不斷地刷新,不停呈現(xiàn)新的幀,就能呈現(xiàn)出連續(xù)的影像。而這個屏幕刷新的頻率,就是幀率(Frame per Second,F(xiàn)PS)。由于人眼的視覺暫留效應(yīng),當屏幕刷新頻率足夠高時(FPS 通常是 50 到 60 左右),就能讓畫面看起來是連續(xù)而流暢的。對于 iOS 而言,app 應(yīng)該盡量保證 60 FPS 才是最好的體驗。
屏幕撕裂 Screen Tearing
在這種單一緩存的模式下,最理想的情況就是一個流暢的流水線:每次電子束從頭開始新的一幀的掃描時,CPU+GPU 對于該幀的渲染流程已經(jīng)結(jié)束,渲染好的位圖已經(jīng)放入幀緩沖器中。但這種完美的情況是非常脆弱的,很容易產(chǎn)生屏幕撕裂:

CPU+GPU 的渲染流程是一個非常耗時的過程。如果在電子束開始掃描新的一幀時,位圖還沒有渲染好,而是在掃描到屏幕中間時才渲染完成,被放入幀緩沖器中 ---- 那么已掃描的部分就是上一幀的畫面,而未掃描的部分則會顯示新的一幀圖像,這就造成屏幕撕裂。
垂直同步 Vsync + 雙緩沖機制 Double Buffering
解決屏幕撕裂、提高顯示效率的一個策略就是使用垂直同步信號 Vsync 與雙緩沖機制 Double Buffering。根據(jù)蘋果的官方文檔描述,iOS 設(shè)備會始終使用 Vsync + Double Buffering 的策略。
垂直同步信號(vertical synchronisation,Vsync)相當于給幀緩沖器加鎖:當電子束完成一幀的掃描,將要從頭開始掃描時,就會發(fā)出一個垂直同步信號。只有當視頻控制器接收到 Vsync 之后,才會將幀緩沖器中的位圖更新為下一幀,這樣就能保證每次顯示的都是同一幀的畫面,因而避免了屏幕撕裂。
但是這種情況下,視頻控制器在接受到 Vsync 之后,就要將下一幀的位圖傳入,這意味著整個 CPU+GPU 的渲染流程都要在一瞬間完成,這是明顯不現(xiàn)實的。所以雙緩沖機制會增加一個新的備用緩沖器(back buffer)。渲染結(jié)果會預(yù)先保存在 back buffer 中,在接收到 Vsync 信號的時候,視頻控制器會將 back buffer 中的內(nèi)容置換到 frame buffer 中,此時就能保證置換操作幾乎在一瞬間完成(實際上是交換了內(nèi)存地址)。

掉幀 Jank
啟用 Vsync 信號以及雙緩沖機制之后,能夠解決屏幕撕裂的問題,但是會引入新的問題:掉幀。如果在接收到 Vsync 之時 CPU 和 GPU 還沒有渲染好新的位圖,視頻控制器就不會去替換 frame buffer 中的位圖。這時屏幕就會重新掃描呈現(xiàn)出上一幀一模一樣的畫面。相當于兩個周期顯示了同樣的畫面,這就是所謂掉幀的情況。

如圖所示,A、B 代表兩個幀緩沖器,當 B 沒有渲染完畢時就接收到了 Vsync 信號,所以屏幕只能再顯示相同幀 A,這就發(fā)生了第一次的掉幀。
三緩沖 Triple Buffering
事實上上述策略還有優(yōu)化空間。我們注意到在發(fā)生掉幀的時候,CPU 和 GPU 有一段時間處于閑置狀態(tài):當 A 的內(nèi)容正在被掃描顯示在屏幕上,而 B 的內(nèi)容已經(jīng)被渲染好,此時 CPU 和 GPU 就處于閑置狀態(tài)。那么如果我們增加一個幀緩沖器,就可以利用這段時間進行下一步的渲染,并將渲染結(jié)果暫存于新增的幀緩沖器中。

如圖所示,由于增加了新的幀緩沖器,可以一定程度上地利用掉幀的空檔期,合理利用 CPU 和 GPU 性能,從而減少掉幀的次數(shù)。
屏幕卡頓的本質(zhì)
手機使用卡頓的直接原因,就是掉幀。前文也說過,屏幕刷新頻率必須要足夠高才能流暢。對于 iPhone 手機來說,屏幕最大的刷新頻率是 60 FPS,一般只要保證 50 FPS 就已經(jīng)是較好的體驗了。但是如果掉幀過多,導致刷新頻率過低,就會造成不流暢的使用體驗。
這樣看來,可以大概總結(jié)一下
-
屏幕卡頓的根本原因:CPU 和 GPU 渲染流水線耗時過長,導致掉幀。 -
Vsync 與雙緩沖的意義:強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。 -
三緩沖的意義:合理使用 CPU、GPU 渲染性能,減少掉幀次數(shù)。
3. iOS 中的渲染框架

iOS 的渲染框架依然符合渲染流水線的基本架構(gòu),具體的技術(shù)棧如上圖所示。在硬件基礎(chǔ)之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多種軟件框架來繪制內(nèi)容,在 CPU 與 GPU 之間進行了更高層地封裝。
GPU Driver:上述軟件框架相互之間也有著依賴關(guān)系,不過所有框架最終都會通過 OpenGL 連接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代碼塊,直接與 GPU 連接。
OpenGL:是一個提供了 2D 和 3D 圖形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,實現(xiàn)硬件加速渲染。OpenGL的高效實現(xiàn)(利用了圖形加速硬件)一般由顯示設(shè)備廠商提供,而且非常依賴于該廠商提供的硬件。OpenGL 之上擴展出很多東西,如 Core Graphics 等最終都依賴于 OpenGL,有些情況下為了更高的效率,比如游戲程序,甚至會直接調(diào)用 OpenGL 的接口。
Core Graphics:Core Graphics 是一個強大的二維圖像繪制引擎,是 iOS 的核心圖形庫,常用的比如 CGRect 就定義在這個框架下。
Core Animation:在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,它的自由度更高,使用范圍也更廣。
Core Image:Core Image 是一個高性能的圖像處理分析的框架,它擁有一系列現(xiàn)成的圖像濾鏡,能對已存在的圖像進行高效的處理。
Metal:Metal 類似于 OpenGL ES,也是一套第三方標準,具體實現(xiàn)由蘋果實現(xiàn)。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構(gòu)建于 Metal 之上的。
Core Animation 是什么
Render, compose, and animate visual elements. ---- Apple
Core Animation,它本質(zhì)上可以理解為一個復合引擎,主要職責包含:渲染、構(gòu)建和實現(xiàn)動畫。
通常我們會使用 Core Animation 來高效、方便地實現(xiàn)動畫,但是實際上它的前身叫做 Layer Kit,關(guān)于動畫實現(xiàn)只是它功能中的一部分。對于 iOS app,不論是否直接使用了 Core Animation,它都在底層深度參與了 app 的構(gòu)建。而對于 OS X app,也可以通過使用 Core Animation 方便地實現(xiàn)部分功能。

Core Animation 是 AppKit 和 UIKit 完美的底層支持,同時也被整合進入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和構(gòu)建的最基礎(chǔ)架構(gòu)。Core Animation 的職責就是盡可能快地組合屏幕上不同的可視內(nèi)容,這個內(nèi)容是被分解成獨立的 layer(iOS 中具體而言就是 CALayer),并且被存儲為樹狀層級結(jié)構(gòu)。這個樹也形成了 UIKit 以及在 iOS 應(yīng)用程序當中你所能在屏幕上看見的一切的基礎(chǔ)。
簡單來說就是用戶能看到的屏幕上的內(nèi)容都由 CALayer 進行管理。那么 CALayer 究竟是如何進行管理的呢?另外在 iOS 開發(fā)過程中,最大量使用的視圖控件實際上是 UIView 而不是 CALayer,那么他們兩者的關(guān)系到底如何呢?
CALayer 是顯示的基礎(chǔ):存儲 bitmap
簡單理解,CALayer 就是屏幕顯示的基礎(chǔ)。那 CALayer 是如何完成的呢?讓我們來從源碼向下探索一下,在 CALayer.h 中,CALayer 有這樣一個屬性 contents:
/** Layer content properties and methods. **/
/* An object providing the contents of the layer, typically a CGImageRef,
* but may be something else. (For example, NSImage objects are
* supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */
@property(nullable, strong) id contents;
An object providing the contents of the layer, typically a CGImageRef.
contents 提供了 layer 的內(nèi)容,是一個指針類型,在 iOS 中的類型就是 CGImageRef(在 OS X 中還可以是 NSImage)。而我們進一步查到,Apple 對 CGImageRef 的定義是:
A bitmap image or image mask.
看到 bitmap,這下我們就可以和之前講的的渲染流水線聯(lián)系起來了:實際上,CALayer 中的 contents 屬性保存了由設(shè)備渲染流水線渲染好的位圖 bitmap(通常也被稱為 backing store),而當設(shè)備屏幕進行刷新時,會從 CALayer 中讀取生成好的 bitmap,進而呈現(xiàn)到屏幕上。
所以,如果我們在代碼中對 CALayer 的 contents 屬性進行了設(shè)置,比如這樣:
// 注意 CGImage 和 CGImageRef 的關(guān)系:
// typedef struct CGImage CGImageRef;
layer.contents = (__bridge id)image.CGImage;**
那么在運行時,操作系統(tǒng)會調(diào)用底層的接口,將 image 通過 CPU+GPU 的渲染流水線渲染得到對應(yīng)的 bitmap,存儲于 CALayer.contents 中,在設(shè)備屏幕進行刷新的時候就會讀取 bitmap 在屏幕上呈現(xiàn)。
也正因為每次要被渲染的內(nèi)容是被靜態(tài)的存儲起來的,所以每次渲染時,Core Animation 會觸發(fā)調(diào)用 drawRect: 方法,使用存儲好的 bitmap 進行新一輪的展示。
CALayer 與 UIView 的關(guān)系
UIView 作為最常用的視圖控件,和 CALayer 也有著千絲萬縷的聯(lián)系,那么兩者之間到底是個什么關(guān)系,他們有什么差異?
當然,兩者有很多顯性的區(qū)別,比如是否能夠響應(yīng)點擊事件。但為了從根本上徹底搞懂這些問題,我們必須要先搞清楚兩者的職責。
UIView - Apple
Views are the fundamental building blocks of your app's user interface, and the
UIViewclass defines the behaviors that are common to all views. A view object renders content within its bounds rectangle and handles any interactions with that content.
根據(jù) Apple 的官方文檔,UIView 是 app 中的基本組成結(jié)構(gòu),定義了一些統(tǒng)一的規(guī)范。它會負責內(nèi)容的渲染以及,處理交互事件。具體而言,它負責的事情可以歸為下面三類
-
Drawing and animation:繪制與動畫 -
Layout and subview management:布局與子 view 的管理 -
Event handling:點擊事件處理
CALayer - Apple
Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide...
If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship.
而從 CALayer 的官方文檔中我們可以看出,CALayer 的主要職責是管理內(nèi)部的可視內(nèi)容,這也和我們前文所講的內(nèi)容吻合。當我們創(chuàng)建一個 UIView 的時候,UIView 會自動創(chuàng)建一個 CALayer,為自身提供存儲 bitmap 的地方(也就是前文說的 backing store),并將自身固定設(shè)置為 CALayer 的代理。

從這兒我們大概總結(jié)出下面兩個核心關(guān)系:
-
CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內(nèi)容的呈現(xiàn)。 -
UIView 提供了對 CALayer 部分功能的封裝,同時也另外負責了交互事件的處理。
有了這兩個最關(guān)鍵的根本關(guān)系,那么下面這些經(jīng)常出現(xiàn)在面試答案里的顯性的異同就很好解釋了。舉幾個例子:
-
相同的層級結(jié)構(gòu):我們對 UIView 的層級結(jié)構(gòu)非常熟悉,由于每個 UIView 都對應(yīng) CALayer 負責頁面的繪制,所以 CALayer 也具有相應(yīng)的層級結(jié)構(gòu)。
-
部分效果的設(shè)置:因為 UIView 只對 CALayer 的部分功能進行了封裝,而另一部分如圓角、陰影、邊框等特效都需要通過調(diào)用 layer 屬性來設(shè)置。
-
是否響應(yīng)點擊事件:CALayer 不負責點擊事件,所以不響應(yīng)點擊事件,而 UIView 會響應(yīng)。
-
不同繼承關(guān)系:CALayer 繼承自 NSObject,UIView 由于要負責交互事件,所以繼承自 UIResponder。
當然還剩最后一個問題,為什么要將 CALayer 獨立出來,直接使用 UIView 統(tǒng)一管理不行嗎?為什么不用一個統(tǒng)一的對象來處理所有事情呢?
這樣設(shè)計的主要原因就是為了職責分離,拆分功能,方便代碼的復用。通過 Core Animation 框架來負責可視內(nèi)容的呈現(xiàn),這樣在 iOS 和 OS X 上都可以使用 Core Animation 進行渲染。與此同時,兩個系統(tǒng)還可以根據(jù)交互規(guī)則的不同來進一步封裝統(tǒng)一的控件,比如 iOS 有 UIKit 和 UIView,OS X 則是AppKit 和 NSView。
4. Core Animation 渲染全內(nèi)容
Core Animation Pipeline 渲染流水線
當我們了解了 Core Animation 以及 CALayer 的基本知識后,接下來我們來看下 Core Animation 的渲染流水線。

整個流水線一共有下面幾個步驟:
Handle Events:這個過程中會先處理點擊事件,這個過程中有可能會需要改變頁面的布局和界面層次。
Commit Transaction:此時 app 會通過 CPU 處理顯示內(nèi)容的前置計算,比如布局計算、圖片解碼等任務(wù),接下來會進行詳細的講解。之后將計算好的圖層進行打包發(fā)給 Render Server。
Decode:打包好的圖層被傳輸?shù)?nbsp;Render Server 之后,首先會進行解碼。注意完成解碼之后需要等待下一個 RunLoop 才會執(zhí)行下一步 Draw Calls。
Draw Calls:解碼完成后,Core Animation 會調(diào)用下層渲染框架(比如 OpenGL 或者 Metal)的方法進行繪制,進而調(diào)用到 GPU。
Render:這一階段主要由 GPU 進行渲染。
Display:顯示階段,需要等 render 結(jié)束的下一個 RunLoop 觸發(fā)顯示。
Commit Transaction 發(fā)生了什么
一般開發(fā)當中能影響到的就是 Handle Events 和 Commit Transaction 這兩個階段,這也是開發(fā)者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進行的是:Layout、Display、Prepare、Commit 等四個具體的操作。
Layout:構(gòu)建視圖
這個階段主要處理視圖的構(gòu)建和布局,具體步驟包括:
-
調(diào)用重載的 layoutSubviews方法 -
創(chuàng)建視圖,并通過 addSubview方法添加子視圖 -
計算視圖布局,即所有的 Layout Constraint
由于這個階段是在 CPU 中進行,通常是 CPU 限制或者 IO 限制,所以我們應(yīng)該盡量高效輕量地操作,減少這部分的時間,比如減少非必要的視圖創(chuàng)建、簡化布局計算、減少視圖層級等。
Display:繪制視圖
這個階段主要是交給 Core Graphics 進行視圖的繪制,注意不是真正的顯示,而是得到前文所說的圖元 primitives 數(shù)據(jù):
-
根據(jù)上一階段 Layout 的結(jié)果創(chuàng)建得到圖元信息。 -
如果重寫了 drawRect:方法,那么會調(diào)用重載的drawRect:方法,在drawRect:方法中手動繪制得到 bitmap 數(shù)據(jù),從而自定義視圖的繪制。
注意正常情況下 Display 階段只會得到圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據(jù)圖元信息繪制得到的。但是如果重寫了 drawRect: 方法,這個方法會直接調(diào)用 Core Graphics 繪制方法得到 bitmap 數(shù)據(jù),同時系統(tǒng)會額外申請一塊內(nèi)存,用于暫存繪制好的 bitmap。
由于重寫了 drawRect: 方法,導致繪制過程從 GPU 轉(zhuǎn)移到了 CPU,這就導致了一定的效率損失。與此同時,這個過程會額外使用 CPU 和內(nèi)存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內(nèi)存爆炸。
Prepare:Core Animation 額外的工作
這一步主要是:圖片解碼和轉(zhuǎn)換
Commit:打包并發(fā)送
這一步主要是:圖層打包并發(fā)送到 Render Server。
注意 commit 操作是依賴圖層樹遞歸執(zhí)行的,所以如果圖層樹過于復雜,commit 的開銷就會很大。這也是我們希望減少視圖層級,從而降低圖層樹復雜度的原因。
Rendering Pass:Render Server 的具體操作

Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 為例,那么上圖主要是 GPU 中執(zhí)行的操作,具體主要包括:
-
GPU 收到 Command Buffer,包含圖元 primitives 信息 -
Tiler 開始工作:先通過頂點著色器 Vertex Shader 對頂點進行處理,更新圖元信息 -
平鋪過程:平鋪生成 tile bucket 的幾何圖形,這一步會將圖元信息轉(zhuǎn)化為像素,之后將結(jié)果寫入 Parameter Buffer 中 -
Tiler 更新完所有的圖元信息,或者 Parameter Buffer 已滿,則會開始下一步 -
Renderer 工作:將像素信息進行處理得到 bitmap,之后存入 Render Buffer -
Render Buffer 中存儲有渲染好的 bitmap,供之后的 Display 操作使用
使用 Instrument 的 OpenGL ES,可以對過程進行監(jiān)控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分別監(jiān)控 Tiler 和 Renderer 的工作情況
5. Offscreen Rendering 離屏渲染
離屏渲染作為一個面試高頻問題,時常被提及,下面來從頭到尾講一下離屏渲染。
離屏渲染具體過程
根據(jù)前文,簡化來看,通常的渲染流程是這樣的:

App 通過 CPU 和 GPU 的合作,不停地將內(nèi)容渲染完成放入 Framebuffer 幀緩沖器中,而顯示屏幕不斷地從 Framebuffer 中獲取內(nèi)容,顯示實時的內(nèi)容。
而離屏渲染的流程是這樣的:

與普通情況下 GPU 直接將渲染好的內(nèi)容放入 Framebuffer 中不同,需要先額外創(chuàng)建離屏渲染緩沖區(qū) Offscreen Buffer,將提前渲染好的內(nèi)容放入其中,等到合適的時機再將 Offscreen Buffer 中的內(nèi)容進一步疊加、渲染,完成后將結(jié)果切換到 Framebuffer 中。
離屏渲染的效率問題
從上面的流程來看,離屏渲染時由于 App 需要提前對部分內(nèi)容進行額外的渲染并保存到 Offscreen Buffer,以及需要在必要時刻對 Offscreen Buffer 和 Framebuffer 進行內(nèi)容切換,所以會需要更長的處理時間(實際上這兩步關(guān)于 buffer 的切換代價都非常大)。
并且 Offscreen Buffer 本身就需要額外的空間,大量的離屏渲染可能早能內(nèi)存的過大壓力。與此同時,Offscreen Buffer 的總大小也有限,不能超過屏幕總像素的 2.5 倍。
可見離屏渲染的開銷非常大,一旦需要離屏渲染的內(nèi)容過多,很容易造成掉幀的問題。所以大部分情況下,我們都應(yīng)該盡量避免離屏渲染。
為什么使用離屏渲染
那么為什么要使用離屏渲染呢?主要是因為下面這兩種原因:
-
一些特殊效果需要使用額外的 Offscreen Buffer 來保存渲染的中間狀態(tài),所以不得不使用離屏渲染。 -
處于效率目的,可以將內(nèi)容提前渲染保存在 Offscreen Buffer 中,達到復用的目的。
對于第一種情況,也就是不得不使用離屏渲染的情況,一般都是系統(tǒng)自動觸發(fā)的,比如陰影、圓角等等。
最常見的情形之一就是:使用了 mask 蒙版。

如圖所示,由于最終的內(nèi)容是由兩層渲染結(jié)果疊加,所以必須要利用額外的內(nèi)存空間對中間的渲染結(jié)果進行保存,因此系統(tǒng)會默認觸發(fā)離屏渲染。
又比如下面這個例子,iOS 8 開始提供的模糊特效 UIBlurEffectView:

整個模糊過程分為多步:Pass 1 先渲染需要模糊的內(nèi)容本身,Pass 2 對內(nèi)容進行縮放,Pass 3 4 分別對上一步內(nèi)容進行橫縱方向的模糊操作,最后一步用模糊后的結(jié)果疊加合成,最終實現(xiàn)完整的模糊特效。
而第二種情況,為了復用提高效率而使用離屏渲染一般是主動的行為,是通過 CALayer 的 shouldRasterize 光柵化操作實現(xiàn)的。
shouldRasterize 光柵化
When the value of this property is
YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
開啟光柵化后,會觸發(fā)離屏渲染,Render Server 會強制將 CALayer 的渲染位圖結(jié)果 bitmap 保存下來,這樣下次再需要渲染時就可以直接復用,從而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圓角、陰影、組透明度 group opacity 等,所以如果 layer 的構(gòu)成包含上述幾種元素,結(jié)構(gòu)復雜且需要反復利用,那么就可以考慮打開光柵化。
圓角、陰影、組透明度等會由系統(tǒng)自動觸發(fā)離屏渲染,那么打開光柵化可以節(jié)約第二次及以后的渲染時間。而多層 subLayer 的情況由于不會自動觸發(fā)離屏渲染,所以相比之下會多花費第一次離屏渲染的時間,但是可以節(jié)約后續(xù)的重復渲染的開銷。
不過使用光柵化的時候需要注意以下幾點:
-
如果 layer 不能被復用,則沒有必要打開光柵化 -
如果 layer 不是靜態(tài),需要被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率 -
離屏渲染緩存內(nèi)容有時間限制,緩存內(nèi)容 100ms 內(nèi)如果沒有被使用,那么就會被丟棄,無法進行復用 -
離屏渲染緩存空間有限,超過 2.5 倍屏幕像素大小的話也會失效,無法復用
圓角的離屏渲染
通常來講,設(shè)置了 layer 的圓角效果之后,會自動觸發(fā)離屏渲染。但是究竟什么情況下設(shè)置圓角才會觸發(fā)離屏渲染呢?

如上圖所示,layer 由三層組成,我們設(shè)置圓角通常會首先像下面這行代碼一樣進行設(shè)置:
view.layer.cornerRadius = 2
根據(jù) cornerRadius - Apple 的描述,上述代碼只會默認設(shè)置 backgroundColor 和 border 的圓角,而不會設(shè)置 content 的圓角,除非同時設(shè)置了 layer.masksToBounds 為 true(對應(yīng) UIView 的 clipsToBounds 屬性):
Setting the radius to a value greater than
0.0causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’scontentsproperty; it applies only to the background color and border of the layer. However, setting themasksToBoundsproperty totruecauses the content to be clipped to the rounded corners.
如果只是設(shè)置了 cornerRadius 而沒有設(shè)置 masksToBounds,由于不需要疊加裁剪,此時是并不會觸發(fā)離屏渲染的。而當設(shè)置了裁剪屬性的時候,由于 masksToBounds 會對 layer 以及所有 subLayer 的 content 都進行裁剪,所以不得不觸發(fā)離屏渲染。
view.layer.masksToBounds = true // 觸發(fā)離屏渲染的原因
所以,Texture 也提出在沒有必要使用圓角裁剪的時候,盡量不去觸發(fā)離屏渲染而影響效率:

離屏渲染的具體邏輯
剛才說了圓角加上 masksToBounds 的時候,因為 masksToBounds 會對 layer 上的所有內(nèi)容進行裁剪,從而誘發(fā)了離屏渲染,那么這個過程具體是怎么回事呢,下面我們來仔細講一下。
圖層的疊加繪制大概遵循“畫家算法”,在這種算法下會按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分。

在普通的 layer 繪制中,上層的 sublayer 會覆蓋下層的 sublayer,下層 sublayer 繪制完之后就可以拋棄了,從而節(jié)約空間提高效率。所有 sublayer 依次繪制完畢之后,整個繪制過程完成,就可以進行后續(xù)的呈現(xiàn)了。假設(shè)我們需要繪制一個三層的 sublayer,不設(shè)置裁剪和圓角,那么整個繪制過程就如下圖所示:

而當我們設(shè)置了 cornerRadius 以及 masksToBounds 進行圓角 + 裁剪時,如前文所述,masksToBounds 裁剪屬性會應(yīng)用到所有的 sublayer 上。這也就意味著所有的 sublayer 必須要重新被應(yīng)用一次圓角+裁剪,這也就意味著所有的 sublayer 在第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer 中等待下一輪圓角+裁剪,這也就誘發(fā)了離屏渲染,具體過程如下:

實際上不只是圓角+裁剪,如果設(shè)置了透明度+組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset 等)都會產(chǎn)生類似的效果,因為組透明度、陰影都是和裁剪類似的,會作用與 layer 以及其所有 sublayer 上,這就導致必然會引起離屏渲染。
避免圓角離屏渲染
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎?
由于剛才我們提到,圓角引起離屏渲染的本質(zhì)是裁剪的疊加,導致 masksToBounds 對 layer 以及所有 sublayer 進行二次處理。那么我們只要避免使用 masksToBounds 進行二次處理,而是對所有的 sublayer 進行預(yù)處理,就可以只進行“畫家算法”,用一次疊加就完成繪制。
那么可行的實現(xiàn)方法大概有下面幾種:
-
【換資源】直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法需要依賴具體情況,并不通用。 -
【mask】再增加一個和背景色相同的遮罩 mask 覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。 -
【UIBezierPath】用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設(shè)置只有內(nèi)部可見,再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是 layer 的布局一旦改變,貝塞爾曲線都需要手動地重新繪制,所以需要對 frame、color 等進行手動地監(jiān)聽并重繪。 -
【CoreGraphics】重寫 drawRect:,用 CoreGraphics 相關(guān)方法,在需要應(yīng)用圓角時進行手動繪制。不過 CoreGraphics 效率也很有限,如果需要多次調(diào)用也會有效率問題。
觸發(fā)離屏渲染原因的總結(jié)
總結(jié)一下,下面幾種情況會觸發(fā)離屏渲染:
-
使用了 mask 的 layer ( layer.mask) -
需要進行裁剪的 layer ( layer.masksToBounds/view.clipsToBounds) -
設(shè)置了組透明度為 YES,并且透明度不為 1 的 layer ( layer.allowsGroupOpacity/layer.opacity) -
添加了投影的 layer ( layer.shadow*) -
采用了光柵化的 layer ( layer.shouldRasterize) -
繪制了文字的 layer ( UILabel,CATextLayer,Core Text等)
不過,需要注意的是,重寫 drawRect: 方法并不會觸發(fā)離屏渲染。前文中我們提到過,重寫 drawRect: 會將 GPU 中的渲染操作轉(zhuǎn)移到 CPU 中完成,并且需要額外開辟內(nèi)存空間。但根據(jù)蘋果工程師的說法,這和標準意義上的離屏渲染并不一樣,在 Instrument 中開啟 Color offscreen rendered yellow 調(diào)試時也會發(fā)現(xiàn)這并不會被判斷為離屏渲染。
6. 自測題目
一般來說做點題才能加深理解和鞏固,所以這里從文章里簡單提煉了一些,希望能幫到大家:
-
CPU 和 GPU 的設(shè)計目的分別是什么? -
CPU 和 GPU 哪個的 Cache\ALU\Control unit 的比例更高? -
計算機圖像渲染流水線的大致流程是什么? -
Framebuffer 幀緩沖器的作用是什么? -
Screen Tearing 屏幕撕裂是怎么造成的? -
如何解決屏幕撕裂的問題? -
掉幀是怎么產(chǎn)生的? -
CoreAnimation 的職責是什么? -
UIView 和 CALayer 是什么關(guān)系?有什么區(qū)別? -
為什么會同時有 UIView 和 CALayer,能否合成一個? -
渲染流水線中,CPU 會負責哪些任務(wù)? -
離屏渲染為什么會有效率問題? -
什么時候應(yīng)該使用離屏渲染? -
shouldRasterize 光柵化是什么? -
有哪些常見的觸發(fā)離屏渲染的情況? -
cornerRadius 設(shè)置圓角會觸發(fā)離屏渲染嗎? -
圓角觸發(fā)的離屏渲染有哪些解決方案? -
重寫 drawRect 方法會觸發(fā)離屏渲染嗎?
參考文獻:
-
Inside look at modern web browser - Google -
1.2CPU和GPU的設(shè)計區(qū)別 - Magnum Programm Life -
CUDA編程(三): GPU架構(gòu)了解一下! - SeanDepp -
Graphics pipeline - Wiki -
GPU Rendering Pipeline——GPU渲染流水線簡介 - 拓荒犬的文章 - 知乎 -
計算機那些事(8)——圖形圖像渲染原理 - 楚權(quán)的世界 -
iOS 保持界面流暢的技巧 - ibireme -
Framebuffer - Wiki -
Frame Rate (iOS and tvOS) - Apple -
理解 Vsync - 陳蒙 -
Getting Pixels onto the Screen - objc.io -
深入理解 iOS Rendering Process - lision -
Core Animation Programming Guide - Apple -
iOS Core Animation: Advanced Techniques中文譯本 -
Advanced Graphics and Animations for iOS Apps - Joakim -
iOS 圖像渲染原理 - chuquan -
Texture - Corner Rounding -
Mastering Offscreen Render - seedante -
關(guān)于iOS離屏渲染的深入研究 -
Offscreen rendering / Rendering on the CPU - Stack Overflow
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
