作者 | 尚懷軍
計算機或手機的渲染是一個非常復雜的過程,本文介紹了渲染相關的一些基礎知識,并結合 iOS 和安卓的技術框架介紹了移動端渲染原理,最后詳細的解析了 iOS 中的離屏渲染以及圓角優化的一些方法。
渲染基礎知識
屏幕繪制的原始數據源
位圖
我們在屏幕上繪制圖像需要的原始數據叫做位圖。位圖(Bitmap) 是一種數據結構。一個位圖是由 n*m 個像素組成,每個像素的顏色信息由 RGB 組合或者灰度值表示。根據位深度,可將位圖分為 1、4、8、16、24 及 32 位圖像等。每個像素使用的信息位數越多,可用的顏色就越多,顏色表現就越逼真,越豐富,相應的數據量越大。
物理像素和邏輯像素
位圖一般存儲的是物理像素,而應用層一般用的是邏輯像素,物理像素和邏輯像素之間會存在一定的對應關系。例如,iOS 中物理像素和邏輯像素的對應關系如下:
- iOS1 倍屏 1pt 對應 1 個物理像素
- iOS2 倍屏 1pt 對應 2 個物理像素
- iOS3 倍屏 1pt 對應 3 個物理像素
將位圖繪制到顯示器
上邊講了屏幕上繪制圖像需要的原始數據叫做位圖。那么問題來了,有了位圖數據之后如何將圖像繪制到屏幕上呢?如下圖所示:電子槍從上到下逐行掃描,掃描完成后顯示器就呈現一幀畫面。然后電子槍回到屏幕初始位置進行下一次掃描。為了同步顯示器的顯示過程和視頻控制器的掃描過程,顯示器會用硬件時鐘產生一系列的定時信號。當電子槍換行進行掃描時,顯示器會發出一個水平同步信號;當一幀畫面繪制完成后,電子槍回到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號。顯示器通常以固定的頻率進行刷新,這個刷新率就是垂直同步信號產生的頻率。
CPU、GPU、顯示器協同工作流程
前一部分介紹了視頻控制器將位圖數據顯示到物理屏幕上的過程,那么位圖數據是怎么得到的呢?其實位圖數據是通過 CPU、GPU 協同工作得到的。下圖就是常見的 CPU、GPU、顯示器協同工作的流程。CPU 計算好顯示內容提交至 GPU,GPU 渲染完成后將渲染結果存入幀緩沖區,接下來就需要將得到的像素信息顯示在物理屏幕上了,這時候視頻控制器(Video Controller)會讀取幀緩沖器中的信息傳遞給顯示器(Monitor)進行顯示。完整的流程如下圖所示:
CPU 和 GPU 的區別
講到 CPU、GPU、顯示器的協同工作流程,就不得不提一下 CPU 和 GPU 的區別。
CPU是中央處理器,適合單一復雜邏輯,而GPU是圖形處理器,適合高并發簡單邏輯。
GPU 有特別多的的計算單元和超長的流水線,但是控制邏輯非常簡單,并且還省去了緩存,適合對低延遲要求不高的運算。而 CPU 不僅被 Cache 占據了大量空間,而且還有特別復雜的控制邏輯,相比之下計算能力只是 CPU 很小的一部分。圖形渲染涉及到的矩陣運算比較多,矩陣相關的運算可以被拆分成并行的簡單的運算,所以渲染處理這件事特別適合 GPU 去做。
總結來說:GPU 的工作計算量大,但技術含量不高,需要簡單重復很多次。就好比有個工作需要算成百上千次一百以內加減乘除一樣。而 CPU 就像老教授,積分微分都會算,適合處理單一復雜邏輯運算。
通用渲染流水線
我們通常將圖像繪制的完整流程稱為渲染流水線,這個過程是 CPU 和 GPU 協作完成的。一般一個渲染流程可以分成 4 個概念階段,分別是:應用階段(Application Stage),幾何階段(Geometry Stage),光柵化階段(Rasterizer Stage),像素處理階段(Pixel Processing)。在《Real–Time Rendering 4th》中非常透徹的講解了實時渲染的各種知識點,對渲染原理感興趣的可以看看這本書,這本書堪稱“實時渲染圣經”。下邊會簡單介紹一下這幾個過程。
應用階段(Application Stage)
簡而言之,就是圖像在應用中的處理階段。說白了就是一段運行在 CPU 上的程序,這時還沒有 GPU 什么事。這一階段主要是 CPU 負責處理用戶的交互和操作,然后做一些應用層布局相關的處理,最后輸出圖元(點、線和三角形)信息給到下一階段。
大家可能會疑惑,圖元只有簡單的點、線、三角形,能表示豐富的立體圖形么,下邊這張立體感很強的海豚就能給出肯定的答案了,簡單的三角形再加上不同的著色,就能呈現出立體圖形。
幾何階段(Geometry Stage)
1. 頂點著色器(Vertex Shader)
頂點著色器可以對頂點的屬性進行一些基本的處理。將頂點信息進行視角轉換、添加光照信息、增加紋理等操作。CPU 丟給 GPU 的信息,就好像是站在上帝視角把這個視角看到的所有信息都給到 GPU。而 GPU 則是站在人類的角度,將人類可以觀察到的畫面,輸出在顯示器上。所以這里是以人的視角為中心,進行坐標轉換。
2. 形狀裝配(Shape Assembly)這個階段是將頂點著色器輸出的所有頂點作為輸入,并將所有的點裝配成指定圖元的形狀。圖元(Primitive)如:點、線、三角形。這個階段也叫圖元裝配。
3. 幾何著色器(Geometry Shader)在圖元外添加額外的頂點,將原始圖元轉換成新圖元,來構建更加復雜的模型。
光柵化階段(Rasterizer Stage)
光柵化階段會把前三個幾何階段處理后得到的圖元(primitives)轉換成一系列的像素。
如上圖所示,我們可以看到,每個像素的中心有一個點,光柵化便是用這個中心點來進行劃分的,如果中心點在圖元內部,那么這個中心點所對應的像素就屬于該圖元。簡而言之,這一階段就是將連續的幾何圖形轉化為了離散化的像素點。
像素處理階段(Pixel Processing)
1. 片段著色器(Fragment Shader)
通過上述的光柵化階段之后,我們就拿到了各個圖元對應的像素,最后這個階段要做的事情就是給每個 Pixel 填充上正確的顏色,然后通過一系列處理計算,得到相應的圖像信息,最終輸出到顯示器上。這里會做內插,就像補間動畫一樣。比如想要把一系列散點連成平滑曲線,相鄰的已知點之間可能會缺少很多點,這時候就需要通過內插填補缺少的數據,最終平滑曲線上除已知點之外的所有點都是插值得到的。同樣的,三角形的三個角色值給定后,其它的片段則根據插值計算出來,也就呈現來漸變的效果。
2. 測試與混合(Tests and Blending)
這個階段會檢測對應的深度值(z 坐標),來判斷這個像素位于其它圖層像素的前面還是后面,決定是否應該丟棄。此外,該階段還會檢查 alpha? 值( alpha 值定義了一個像素的透明度),從而對圖層進行混合。( 一句話簡單說,就是檢查圖層深度和透明度,并進行圖層混合。)
R = S + D * (1 - Sa)
含義:
R:Result,最終像素顏色。
S:Source,來源像素(上面的圖層像素)。
D:Destination,目標像素(下面的圖層像素)。
a:alpha,透明度。
結果 = S(上)的顏色 + D(下)的顏色 * (1 - S(上)的透明度)
經歷了上邊漫長的流水線之后我們便可以拿到屏幕繪制所需要的原始數據源-位圖數據,然后由視頻控制器把位圖數據顯示在物理屏幕上。
iOS 渲染原理
渲染技術棧
上邊鋪墊完渲染相關的一些基礎知識之后,下面主要介紹 iOS 渲染相關的一些原理和知識。下圖是 iOS 的圖形渲染技術棧,有三個相關的核心系統框架:Core Graphics、Core Animation、Core Image ,這三個框架主要用來繪制可視化內容。他們都是通過 OpenGL 來調用 GPU 進行實際的渲染,然后生成最終位圖數據存儲到幀緩沖區,視頻控制器再將幀緩沖區的數據顯示物理屏幕上。
UIKit
UIKit 是 iOS 開發者最常用的框架,可以通過設置 UIKit 組件的布局以及相關屬性來繪制界面。但是 UIKit 并不具備在屏幕成像的能力,這個框架主要負責對用戶操作事件的響應(UIView 繼承自 UIResponder),事件經過響應鏈傳遞。
Core Animation
Core Animation 主要負責組合屏幕上不同的可視內容,這些可視內容可被分解成獨立的圖層也就是我們日常開發過程中常接觸的 CALayer,這些圖層被存儲在圖層樹中。CALayer 主要負責頁面渲染,它是用戶能在屏幕上看見的一切的基礎。
Core Graphics
Core Graphics 主要用于運行時繪制圖像。開發者可以使用此框架來處理基于路徑的繪圖,轉換,顏色管理,離屏渲染,圖案,漸變和陰影等等。
Core Image
Core Image 與 Core Graphics 正好相反,Core Graphics 是在運行時創建圖像,而 Core Image 則是在運行前創建圖像。
OpenGL ES 和 Metal
OpenGL ES 和 Metal 都是第三方標準,基于這些標準具體的內部實現是由對應的 GPU 廠商開發的。Metal 是蘋果的一套第三方標準,由蘋果實現。很多開發者都沒有直接使用過 Metal,但卻通過 Core Animation、Core Image 這些核心的系統框架在間接的使用 metal。
CoreAnimation 與 UIKit 框架的關系
上邊渲染框架中提到的 Core Animation 是 iOS 和 OS X 上圖形渲染和動畫的基礎框架,主要用來給視圖和應用程序的其他可視元素設置動畫。Core Animation 的實現邏輯是將大部分實際繪圖的工作交給 GPU 加速渲染,這樣不會給 CPU 帶來負擔,還能實現流暢的動畫。CoreAnimation 的核心類是 CALayer,UIKit 框架的核心類是 UIView,下邊詳細介紹一下這兩個類的關系。
UIView 與 CALayer 的關系
如上圖所示,UIView 和 CALayer 是一一對應的關系,每一個 UIView 都有一個 CALayer 與之對應,一個負責布局、交互響應,一個負責頁面渲染。
他們的兩個核心關系如下:
- CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
- UIView 提供了對 CALayer 功能的封裝,負責了交互事件的處理。
舉一個形象一點的例子,UIView 是畫板,CALayer 就是畫布,當你創建一個畫板的時候,會自動綁定一個畫布,畫板會響應你的操作,比如你可以移動畫板,畫布則負責呈現具體的圖形,二者職責分明。一個負責交互,一個負責渲染繪制。
為什么要分離出 CALayer 和 UIView?
iOS 平臺和 MacOS 平臺上用戶的交互方式有著本質的不同,但是渲染邏輯是通用的,在 iOS 系統中我們使用的是 UIKit 和 UIView,而在 MacOS 系統中我們使用的是 AppKit 和 NSView,所以在這種情況下將展示部分的邏輯分離出來跨平臺復用。
CALayer 中的 contents 屬性保存了由設備渲染流水線渲染好的位圖 bitmap(通常被稱為 backing store),也就是我們最開始說的屏幕繪制需要的最原始的數據源。而當設備屏幕進行刷新時,會從 CALayer 中讀取生成好的 bitmap,進而呈現到屏幕上。
@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
/** 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;
@end
Core Animation 流水線
其實早在 WWDC 的 Advanced Graphics and Animations for iOS Apps(WWDC14 419,關于 UIKit 和 Core Animation 基礎的 session)中蘋果就給出了 CoreAnimation 框架的渲染流水線,具體流程如下圖所示:
整個流水線中 app 本身并不負責渲染,渲染則是由一個獨立的進程負責,即 Render Server 進程。下邊會詳細介紹一下整個 pipeline 的流程。
應用階段
- 視圖的創建
- 布局計算
- 對圖層進行打包,在下一次 RunLoop 時將其發送至Render Server
- app 處理用戶的點擊操作,在這個過程中 app 可能需要更新視圖樹,如果視圖樹發生更新,圖層樹也會被更新
- 其次,app 通過 CPU 完成對顯示內容的計算
Render Server & GPU
- 這一階段主要執行 metal、Core Graphics 等相關程序,并調用 GPU 在物理層上完成對圖像的渲染
- GPU 將渲染后的位圖數據存儲到 Frame Buffer
Display
- 視頻控制器將幀緩沖區的位圖數據一幀一幀的顯示在物理屏幕上
如果把把上邊的步驟串在一起,會發現它們執行消耗的時間超過了 16.67 ms,因此為了滿足對屏幕的 60 FPS 刷新率的支持,需要通過流水線的方式將這些步驟并行執行,如下圖所示。每一個階段都在源源不斷的給下一個階段輸送產物。這時候就可以滿足 16.67 毫秒產生一幀數據的要求了。
安卓渲染原理
安卓上層顯示系統
安卓中 Activity 的一個重要的職責就是對界面生命周期的管理,這也就伴隨了對視圖窗口的管理。這中間就涉及了兩個 Android 中兩個主要的服務,AMS(ActivityManagerService)和WMS(WindowManagerService)。
在 Android 中,一個 view 會有對應的 canvas。視圖樹對應一個 canvas 樹,Surfaceflinger 控制多個 canvas 的合成。最終渲染完成輸出位圖數據,顯示到手機屏幕。
應用層布局
View 和 ViewGroup
View 是 Android 中所有控件的基類,View 類有一個很重要的子類:ViewGroup,ViewGroup 作為其他 view 的容器使用。Android 的所有 UI 組件都是建立在 View、ViewGroup 基礎之上的,整體采用“組合”的思想來設計 View 和 ViewGroup:ViewGroup 是 View 的子類,所以 ViewGroup 也可以被當做 View 使用。一個 Android app 的圖形用戶界面會對應一個視圖樹,而視圖樹則對應一個 canvas 樹。這個有點兒類似于 iOS 中的 UIView 和 CALayer 的概念,一個負責應用層布局,一個負責底層渲染。
系統底層渲染顯示
應用層的 view 對應到 canvas,canvas 到系統進程就成了 layer。SurfaceFlinger 主要提供 layer 的渲染合成服務。SurfaceFlinger 是一個常駐的 binder 服務,會隨著 init 進程的啟動而啟動。下面這張圖就詳細的介紹了上層 view 到底層 layer 的轉化,以及 SurfaceFlinger 對多個 layer 的渲染合成。
iOS 離屏渲染
離屏渲染原理以及定義
首先來介紹一下離屏渲染的原理。我們正常的渲染流程是:CPU 和 GPU 協作,不停地將內容渲染完成后得到的位圖數據放入 Framebuffer (幀緩沖區)中,視頻控制器則不斷地從 Framebuffer 中獲取內容,顯示實時的內容。
而離屏渲染的流程是這樣的:
與普通情況下 GPU 直接將渲染好的內容放入 Framebuffer 中不同,離屏渲染需要先額外創建離屏渲染緩沖區 ,將提前渲染好的內容放入其中,等到合適的時機再將 Offscreen Buffer 中的內容進一步疊加、渲染,完成后將結果再寫入 Framebuffer 中。
為什么先要將數據存放在離屏渲染緩沖區呢?有兩個原因,一個是被動的,一個是主動的。
- 一些特殊效果需要使用額外的 Offscreen Buffer 來保存渲染的中間狀態(被動)
- 處于效率目的,可以將內容提前渲染保存在 Offscreen Buffer 中,達到復用的目的。(主動)
被動離屏渲染
常見的觸發被動離屏渲染的場景
透明、陰影加圓角通常被稱為 UI 三大寶,但這些效果在 iOS 的日常開發過程中卻往往會導致被動的離屏渲染,下邊是幾個常見的會觸發被動離屏渲染的場景。
觸發離屏渲染的原因
講離屏渲染的原因不得不提畫家算法,畫家算法的整體思想是按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分。這里的層在 iOS 的渲染技術棧中就可以被對應到 layer。
通常對于每一層 layer,Render Server 會遵循“畫家算法”,按次序輸出到 frame buffer,后一層覆蓋前一層,就能得到最終的顯示結果,對于這個 layer 樹則是以深度優先的算法將 layer 輸出到 frame buffer。
作為“畫家”的 GPU 雖然可以一層一層往畫布上進行輸出,但是卻沒有辦法在某一層渲染完成之后,再回過頭來改變其中的某個部分。因為在這一層之前的若干層 layer 像素數據,已經在渲染中被合成在一起了。其實這里和 photoshop 中的圖層合并非常像,一旦多個圖層被合并在一起,就無法再單獨對某一個圖層進行修改。所以需要在離屏緩沖區中把子 layer 依次畫好,然后把四個角裁剪好之后再和之前的圖層進行混合。
GPU 離屏渲染的性能影響
一提離屏渲染,我們直觀上的感覺是會對性能有影響。因為為了滿足 60fps 的刷新頻率,GPU 的操作都是高度流水線化的。本來所有的計算工作都在有條不紊地正在向 frame buffer 輸出,這時候突然又有一些特殊的效果觸發了離屏渲染,需要切換上下文,把數據輸出到另一塊內存,這時候流水線中很多中間產物只能被丟棄,這種頻繁的上下文切換對 GPU 的渲染性能有非常大的影響。
如何防止非必要離屏渲染?
- 對于一些圓角可以創建四個背景顏色弧形的 layer 蓋住四個角,從視覺上制造圓角的效果
- 對于 view 的圓形邊框,如果沒有 backgroundColor,可以放心使用 cornerRadius 來做
- 對于所有的陰影,使用 shadowPath 來規避離屏渲染
- 對于特殊形狀的 view,使用 layer mask 并打開 shouldRasterize 來對渲染結果進行緩存
圓角實現的優化策略
使用CALayer的cornerRadius并設置 cliptobounds 以后會觸發離屏渲染(offscreen rendering)。滾動時每秒需要在 60 幀上執行裁剪操作,即使內容沒有發生任何變化。GPU 也必須在每幀之間切換上下文,合成整個幀和裁剪。這些對性能的消耗直接影響到 Render Server 這個獨立渲染進程,造成掉幀。為了優化渲染性能,我們可以選擇一些其他的實現圓角的方案。下邊是圓角的具體實現需要考慮的條件。
圓角的具體實現需要考慮的條件
- 圓角下(movement underneath the corner)是否有滑動。
- 是否有穿過圓角滑動(movement through the corner)。
- 四個圓角是否處于同一個 layer 上,有沒有與其他 子 layer 相交。
圓角的具體實現方案
如何根據對應的條件選取圓角的實現方案
上邊提到了圓角的優化要考慮的條件以及不同的圓角實現方案,下邊這個流程圖就是把條件和方案對應起來,給出了圓角的最佳實現方案。
總結
本文主要介紹了移動端渲染原理的相關內容。文章開始介紹了一下渲染相關的基礎知識,講了渲染所需要的原始數據源-位圖以及 CPU 和 GPU 如何協同工作得到位圖數據的。后面又結合 iOS 和安卓的技術框架介紹了移動端渲染的相關原理。最后深入分析了 iOS 中的離屏渲染,講解了現有的圓角優化的一些方案。