渲染
渲染原理
一個硬核硬件科普視頻
CPU和GPU
- CPU(Central Processing Unit):現代計算機整個系統的運算核心、控制核心,適合串行計算。
- GPU(Graphics Processing Unit):可進行繪圖運算工作的專用微處理器,是連接計算機和顯示終端的紐帶,適合并行計算。
圖像渲染流水線
圖像渲染的計算量非常大,所以我們不能消耗大量CPU的資源去計算,轉而使用擁有更強計算能力的GPU。
渲染流水線圖如下。
GPU渲染流程圖如下。
Application 應用處理階段:得到圖元
這個階段具體指的就是圖像在應用中被處理的階段,此時還處于 CPU 負責的時期。在這個階段應用可能會對圖像進行一系列的操作或者改變,最終將新的圖像信息傳給下一階段。這部分信息被叫做圖元(primitives),通常是三角形、線段、頂點等,下面這張立體感很強的海豚就很直觀。
Geometry 幾何處理階段:處理圖元
這個階段及之后就由GPU負責,這一階段GPU拿到上一階段圖元信息,處理后輸出新的圖元,對應GPU渲染流程的以下操作:
- 頂點著色器(Vertex Shader):3D坐標轉2D坐標,確定頂點。
- 形狀裝配(Shape Assembly):連接頂點成線。
- 幾何著色器(Geometry Shader):確定三角形的個數,使之變成幾何圖形。
Rasterization 光柵化階段:圖元轉換為像素
將幾何渲染之后的圖元信息,轉換為一系列的像素,以便后續顯示在屏幕上。
Pixel 像素處理階段:處理像素,得到位圖
經過光柵化階段得到了圖元所對應的像素,此時,我們需要給這些像素填充顏色和效果。所以最后這個階段就是給像素填充正確的內容,最終顯示在屏幕上。這些經過處理、蘊含大量信息的像素點集合,被稱作位圖(bitmap)。也就是說,Pixel 階段最終輸出的結果就是位圖,過程具體包含:
- 片段著色器(Fragment Shader):對屏幕像素點著色。
- 測試與混合(Tests and Blending):檢查圖層深度和透明度,從而判斷片段的前后位置,以及是否應該被舍棄,并進行圖層混合。
位圖:
位圖(Bitmap) 是一種數據結構。一個位圖是由 n*m 個像素組成,每個像素的顏色信息由 RGB 組合或者灰度值表示。
屏幕成像與卡頓
在圖像渲染流程結束之后,接下來就需要將得到的像素信息顯示在物理屏幕上了。GPU 最后一步渲染結束之后像素信息,被存在幀緩沖器(Framebuffer)中,之后視頻控制器(Video Controller)會讀取幀緩沖器中的信息,經過數模轉換傳遞給顯示器(Monitor),進行顯示。完整的流程如下圖。
經過 GPU 處理之后的像素集合,也就是位圖,會被幀緩沖器緩存起來,供之后的顯示使用。顯示器的電子束會從屏幕的左上角開始逐行掃描,屏幕上的每個點的圖像信息都從幀緩沖器中的位圖進行讀取,在屏幕上對應地顯示。掃描的流程如下圖所示:
電子束掃描的過程中,屏幕就能呈現出對應的結果,每次整個屏幕被掃描完一次后,就相當于呈現了一幀完整的圖像。屏幕不斷地刷新,不停呈現新的幀,就能呈現出連續的影像。而這個屏幕刷新的頻率,就是幀率(Frame per Second,FPS)。由于人眼的視覺暫留效應,當屏幕刷新頻率足夠高時(FPS 通常是 50 到 60 左右),就能讓畫面看起來是連續而流暢的。對于 iOS 而言,app 應該盡量保證 60 FPS 才是最好的體驗。
屏幕撕裂 Screen Tearing
在這種單一緩存的模式下,最理想的情況就是一個流暢的流水線:每次電子束從頭開始新的一幀的掃描時,CPU+GPU 對于該幀的渲染流程已經結束,渲染好的位圖已經放入幀緩沖器中。但這種完美的情況是非常脆弱的,很容易產生屏幕撕裂:
CPU+GPU 的渲染流程是一個非常耗時的過程。如果在電子束開始掃描新的一幀時,位圖還沒有渲染好,而是在掃描到屏幕中間時才渲染完成,被放入幀緩沖器中 ---- 那么已掃描的部分就是上一幀的畫面,而未掃描的部分則會顯示新的一幀圖像,這就造成屏幕撕裂。
垂直同步 Vsync + 雙緩沖機制 Double Buffering
解決屏幕撕裂、提高顯示效率的一個策略就是使用垂直同步信號 Vsync 與雙緩沖機制 Double Buffering。根據蘋果的官方文檔描述,iOS 設備會始終使用 Vsync + Double Buffering 的策略。
垂直同步信號(vertical synchronisation,Vsync)相當于給幀緩沖器加鎖:當電子束完成一幀的掃描,將要從頭開始掃描時,就會發出一個垂直同步信號。只有當視頻控制器接收到 Vsync 之后,才會將幀緩沖器中的位圖更新為下一幀,這樣就能保證每次顯示的都是同一幀的畫面,因而避免了屏幕撕裂。
但是這種情況下,視頻控制器在接受到 Vsync 之后,就要將下一幀的位圖傳入,這意味著整個 CPU+GPU 的渲染流程都要在一瞬間完成,這是明顯不現實的。所以雙緩沖機制會增加一個新的備用緩沖器(back buffer)。渲染結果會預先保存在 back buffer 中,在接收到 Vsync 信號的時候,視頻控制器會將 back buffer 中的內容置換到 frame buffer 中,此時就能保證置換操作幾乎在一瞬間完成(實際上是交換了內存地址)。
掉幀 Jank
啟用 Vsync 信號以及雙緩沖機制之后,能夠解決屏幕撕裂的問題,但是會引入新的問題:掉幀。如果在接收到 Vsync 之時 CPU 和 GPU 還沒有渲染好新的位圖,視頻控制器就不會去替換 frame buffer 中的位圖。這時屏幕就會重新掃描呈現出上一幀一模一樣的畫面。相當于兩個周期顯示了同樣的畫面,這就是所謂掉幀的情況。
如圖所示,A、B 代表兩個幀緩沖器,當 B 沒有渲染完畢時就接收到了 Vsync 信號,所以屏幕只能再顯示相同幀 A,這就發生了第一次的掉幀。
三緩沖 Triple Buffering
事實上上述策略還有優化空間。我們注意到在發生掉幀的時候,CPU 和 GPU 有一段時間處于閑置狀態:當 A 的內容正在被掃描顯示在屏幕上,而 B 的內容已經被渲染好,此時 CPU 和 GPU 就處于閑置狀態。那么如果我們增加一個幀緩沖器,就可以利用這段時間進行下一步的渲染,并將渲染結果暫存于新增的幀緩沖器中。
如圖所示,由于增加了新的幀緩沖器,可以一定程度上地利用掉幀的空檔期,合理利用 CPU 和 GPU 性能,從而減少掉幀的次數。
屏幕卡頓的本質
手機使用卡頓的直接原因,就是掉幀。前文也說過,屏幕刷新頻率必須要足夠高才能流暢。對于 iPhone 手機來說,屏幕最大的刷新頻率是 60 FPS,一般只要保證 50 FPS 就已經是較好的體驗了。但是如果掉幀過多,導致刷新頻率過低,就會造成不流暢的使用體驗。
總結
- 屏幕卡頓的根本原因:CPU 和 GPU 渲染流水線耗時過長,導致掉幀。
- Vsync 與雙緩沖的意義:強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。
- 三緩沖的意義:合理使用 CPU、GPU 渲染性能,減少掉幀次數。
iOS 中的渲染框架
iOS 的渲染框架依然符合渲染流水線的基本架構,具體的技術棧如上圖所示。在硬件基礎之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多種軟件框架來繪制內容,在 CPU 與 GPU 之間進行了更高層地封裝。
GPU Driver:上述軟件框架相互之間也有著依賴關系,不過所有框架最終都會通過 OpenGL 連接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代碼塊,直接與 GPU 連接。
OpenGL:是一個提供了 2D 和 3D 圖形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,實現硬件加速渲染。
Core Graphics:Core Graphics 是一個強大的二維圖像繪制引擎,是 iOS 的核心圖形庫,常用的比如 CGRect 就定義在這個框架下,最終依賴于 OpenGL。
Core Animation:在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,它的自由度更高,使用范圍也更廣。
Core Image:Core Image 是一個高性能的圖像處理分析的框架,它擁有一系列現成的圖像濾鏡,能對已存在的圖像進行高效的處理。
Metal:Metal 類似于 OpenGL ES,也是一套第三方標準,具體實現由蘋果實現。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構建于 Metal 之上的。
Core Animation
Core Animation,它本質上可以理解為一個復合引擎,主要職責包含:渲染、構建和實現動畫。
Core Animation 是 AppKit 和 UIKit 完美的底層支持,它是 app 界面渲染和構建的最基礎架構。Core Animation 的職責就是盡可能快地組合屏幕上不同的可視內容,這個內容是被分解成獨立的 layer(iOS 中具體而言就是 CALayer),并且被存儲為樹狀層級結構。這個樹也形成了 UIKit 以及在 iOS 應用程序當中你所能在屏幕上看見的一切的基礎。簡單來說就是用戶能看到的屏幕上的內容都由 CALayer 進行管理。
CALayer 是顯示的基礎:存儲 bitmap
簡單理解,CALayer 就是屏幕顯示的基礎。CALayer 有一個屬性 contents,它保存了由設備渲染流水線渲染好的位圖 bitmap,而當設備屏幕進行刷新時,會從 CALayer 中讀取生成好的 bitmap,進而呈現到屏幕上。
每次要被渲染的內容是被靜態的存儲起來的,所以每次渲染時,Core Animation 會觸發調用 drawRect:
方法,使用存儲好的 bitmap 進行新一輪的展示。
CALayer 與 UIView 的關系
即CoreAnimation和UIKit的關系:CoreAnimation 的核心類是 CALayer,UIKit 框架的核心類是 UIView
UIView職責:
- Drawing and animation:繪制與動畫
- Layout and subview management:布局與子 view 的管理
- Event handling:點擊事件處理
當我們創建一個 UIView 的時候,UIView 會自動創建一個 CALayer,為自身提供存儲 bitmap 的地方,并將自身固定設置為 CALayer 的代理。
總結出下面兩個核心關系:
- CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
- UIView 提供了對 CALayer 部分功能的封裝,同時也另外負責了交互事件的處理。
可得出其他關系:
相同的層級結構:我們對 UIView 的層級結構非常熟悉,由于每個 UIView 都對應 CALayer 負責頁面的繪制,所以 CALayer 也具有相應的層級結構。
部分效果的設置:因為 UIView 只對 CALayer 的部分功能進行了封裝,而另一部分如圓角、陰影、邊框等特效都需要通過調用 layer 屬性來設置。
是否響應點擊事件:CALayer 不負責點擊事件,所以不響應點擊事件,而 UIView 會響應。
不同繼承關系:CALayer 繼承自 NSObject,UIView 由于要負責交互事件,所以繼承自 UIResponder。
將 CALayer 獨立出來這樣設計的主要原因就是為了職責分離,拆分功能,方便代碼的復用。通過 Core Animation 框架來負責可視內容的呈現,這樣在 iOS 和 OS X 上都可以使用 Core Animation 進行渲染。與此同時,兩個系統還可以根據交互規則的不同來進一步封裝統一的控件
Core Animation 渲染流程
Core Animation Pipeline 渲染流水線
整個流水線一共有下面幾個步驟:
Handle Events:這個過程中會先處理點擊事件,這個過程中有可能會需要改變頁面的布局和界面層次。
**Commit Transaction:**此時 app 會通過 CPU 處理顯示內容的前置計算,比如布局計算、圖片解碼等任務,之后將計算好的圖層進行打包發給 Render Server
。
**Decode:**打包好的圖層被傳輸到 Render Server
之后,首先會進行解碼。注意完成解碼之后需要等待下一個 RunLoop 才會執行下一步 Draw Calls
。
**Draw Calls:**解碼完成后,Core Animation 會調用下層渲染框架(比如 OpenGL 或者 Metal)的方法進行繪制,進而調用到 GPU。
**Render:**這一階段主要由 GPU 進行渲染。
**Display:**顯示階段,需要等 render
結束的下一個 RunLoop 觸發顯示。
Commit Transaction 發生了什么
一般開發當中能影響到的就是 Handle Events 和 Commit Transaction 這兩個階段,這也是開發者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進行的是:Layout、Display、Prepare、Commit 等四個具體的操作。
Layout:構建視圖
這個階段主要處理視圖的構建和布局,具體步驟包括:
- 調用重載的
layoutSubviews
方法 - 創建視圖,并通過
addSubview
方法添加子視圖 - 計算視圖布局,即所有的 Layout Constraint
由于這個階段是在 CPU 中進行,通常是 CPU 限制或者 IO 限制,所以我們應該盡量高效輕量地操作,減少這部分的時間,比如減少非必要的視圖創建、簡化布局計算、減少視圖層級等。
Display:繪制視圖
這個階段主要是交給 Core Graphics 進行視圖的繪制,注意不是真正的顯示,而是得到前文所說的圖元 primitives 數據:
- 根據上一階段 Layout 的結果創建得到圖元信息。
- 如果重寫了
drawRect:
方法,那么會調用重載的drawRect:
方法,在drawRect:
方法中手動繪制得到 bitmap 數據,從而自定義視圖的繪制。
注意正常情況下 Display 階段只會得到圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據圖元信息繪制得到的。但是如果重寫了 drawRect:
方法,這個方法會直接調用 Core Graphics 繪制方法得到 bitmap 數據,同時系統會額外申請一塊內存,用于暫存繪制好的 bitmap。
由于重寫了 drawRect:
方法,導致繪制過程從 GPU 轉移到了 CPU,這就導致了一定的效率損失。與此同時,這個過程會額外使用 CPU 和內存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內存爆炸。
Prepare:Core Animation 額外的工作
這一步主要是:圖片解碼和轉換
Commit:打包并發送
這一步主要是:當 runloop
在 kCFRunLoopBeforeWaiting
和 kCFRunLoopExit
狀態時,會通知注冊的監聽,然后對圖層打包,打完包后,將打包數據發送給一個獨立負責渲染的進程 Render Server
。圖層打包并發送到 Render Server。
注意 commit 操作是依賴圖層樹遞歸執行的,所以如果圖層樹過于復雜,commit 的開銷就會很大。這也是我們希望減少視圖層級,從而降低圖層樹復雜度的原因。
Rendering Pass: Render Server 的具體操作
Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 為例,那么上圖主要是 GPU 中執行的操作,具體主要包括:
- GPU 收到 Command Buffer,包含圖元 primitives 信息
- Tiler 開始工作:先通過頂點著色器 Vertex Shader 對頂點進行處理,更新圖元信息
- 平鋪過程:平鋪生成 tile bucket 的幾何圖形,這一步會將圖元信息轉化為像素,之后將結果寫入 Parameter Buffer 中
- Tiler 更新完所有的圖元信息,或者 Parameter Buffer 已滿,則會開始下一步
- Renderer 工作:將像素信息進行處理得到 bitmap,之后存入 Render Buffer
- Render Buffer 中存儲有渲染好的 bitmap,供之后的 Display 操作使用
總結流程圖
離屏渲染
離屏渲染具體過程
通常的渲染流程是這樣的,如下圖。
App 通過 CPU 和 GPU 的合作,不停地將內容渲染完成放入 Framebuffer 幀緩沖器中,而顯示屏幕不斷地從 Framebuffer 中獲取內容,顯示實時的內容。
而離屏渲染的流程是這樣的,如下圖。
與普通情況下 GPU 直接將渲染好的內容放入 Framebuffer 中不同,需要先額外創建離屏渲染緩沖區 Offscreen Buffer,將提前渲染好的內容放入其中,等到合適的時機再將 Offscreen Buffer 中的內容進一步疊加、渲染,完成后將結果切換到 Framebuffer 中。
離屏渲染的效率問題
離屏渲染時由于 App 需要提前對部分內容進行額外的渲染并保存到 Offscreen Buffer,以及需要在必要時刻對 Offscreen Buffer 和 Framebuffer 進行內容切換,所以會需要更長的處理時間(實際上這兩步關于 buffer 的切換代價都非常大)。
并且 Offscreen Buffer 本身就需要額外的空間,大量的離屏渲染可能早能內存的過大壓力。與此同時,Offscreen Buffer 的總大小也有限,不能超過屏幕總像素的 2.5 倍。
可見離屏渲染的開銷非常大,一旦需要離屏渲染的內容過多,很容易造成掉幀的問題。所以大部分情況下,我們都應該盡量避免離屏渲染。
為什么使用離屏渲染
- 一些特殊效果需要使用額外的 Offscreen Buffer 來保存渲染的中間狀態,所以不得不使用離屏渲染。
- 處于效率目的,可以將內容提前渲染保存在 Offscreen Buffer 中,達到復用的目的。
對于第一種情況,也就是不得不使用離屏渲染的情況,一般都是系統自動觸發的,比如陰影、圓角等等。
最常見的情形之一就是:使用了 mask 蒙版。
如圖所示,由于最終的內容是由兩層渲染結果疊加,所以必須要利用額外的內存空間對中間的渲染結果進行保存,因此系統會默認觸發離屏渲染。
又比如下面這個例子,iOS 8 開始提供的模糊特效 UIBlurEffectView:
整個模糊過程分為多步:Pass 1 先渲染需要模糊的內容本身,Pass 2 對內容進行縮放,Pass 3 4 分別對上一步內容進行橫縱方向的模糊操作,最后一步用模糊后的結果疊加合成,最終實現完整的模糊特效。
而第二種情況,為了復用提高效率而使用離屏渲染一般是主動的行為,是通過 CALayer 的 shouldRasterize 光柵化操作實現的。
shouldRasterize 光柵化
開啟光柵化后,會觸發離屏渲染,Render Server 會強制將 CALayer 的渲染位圖結果 bitmap 保存下來,這樣下次再需要渲染時就可以直接復用,從而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圓角、陰影、組透明度 group opacity 等,所以如果 layer 的構成包含上述幾種元素,結構復雜且需要反復利用,那么就可以考慮打開光柵化。
圓角、陰影、組透明度等會由系統自動觸發離屏渲染,那么打開光柵化可以節約第二次及以后的渲染時間。而多層 subLayer 的情況由于不會自動觸發離屏渲染,所以相比之下會多花費第一次離屏渲染的時間,但是可以節約后續的重復渲染的開銷。
不過使用光柵化的時候需要注意以下幾點:
- 如果 layer 不能被復用,則沒有必要打開光柵化
- 如果 layer 不是靜態,需要被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率
- 離屏渲染緩存內容有時間限制,緩存內容 100ms 內如果沒有被使用,那么就會被丟棄,無法進行復用
- 離屏渲染緩存空間有限,超過 2.5 倍屏幕像素大小的話也會失效,無法復用
圓角的離屏渲染
通常來講,設置了 layer 的圓角效果之后,會自動觸發離屏渲染。但是究竟什么情況下設置圓角才會觸發離屏渲染呢?
如上圖所示,layer 由三層組成,我們設置圓角通常會首先像下面這行代碼一樣進行設置:
復制代碼view.layer.cornerRadius = 2
根據 cornerRadius - Apple 的描述,上述代碼只會默認設置 backgroundColor 和 border 的圓角,而不會設置 content 的圓角,除非同時設置了 layer.masksToBounds 為 true(對應 UIView 的 clipsToBounds 屬性):
如果只是設置了 cornerRadius 而沒有設置 masksToBounds,由于不需要疊加裁剪,此時是并不會觸發離屏渲染的。而當設置了裁剪屬性的時候,由于 masksToBounds 會對 layer 以及所有 subLayer 的 content 都進行裁剪,所以不得不觸發離屏渲染。
view.layer.masksToBounds = true // 觸發離屏渲染的原因
所以,Texture 也提出在沒有必要使用圓角裁剪的時候,盡量不去觸發離屏渲染而影響效率:
離屏渲染的具體邏輯
剛才說了圓角加上 masksToBounds 的時候,因為 masksToBounds 會對 layer 上的所有內容進行裁剪,從而誘發了離屏渲染,那么這個過程具體是怎么回事呢,下面我們來仔細講一下。
圖層的疊加繪制大概遵循“畫家算法”,在這種算法下會按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分。
在普通的 layer 繪制中,上層的 sublayer 會覆蓋下層的 sublayer,下層 sublayer 繪制完之后就可以拋棄了,從而節約空間提高效率。所有 sublayer 依次繪制完畢之后,整個繪制過程完成,就可以進行后續的呈現了。假設我們需要繪制一個三層的 sublayer,不設置裁剪和圓角,那么整個繪制過程就如下圖所示:
而當我們設置了 cornerRadius 以及 masksToBounds 進行圓角 + 裁剪時,如前文所述,masksToBounds 裁剪屬性會應用到所有的 sublayer 上。這也就意味著所有的 sublayer 必須要重新被應用一次圓角+裁剪,這也就意味著所有的 sublayer 在第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer 中等待下一輪圓角+裁剪,這也就誘發了離屏渲染,具體過程如下:
實際上不只是圓角+裁剪,如果設置了透明度+組透明(layer.allowsGroupOpacity
+layer.opacity
),陰影屬性(shadowOffset
等)都會產生類似的效果,因為組透明度、陰影都是和裁剪類似的,會作用與 layer 以及其所有 sublayer 上,這就導致必然會引起離屏渲染。
避免圓角離屏渲染
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎?
由于剛才我們提到,圓角引起離屏渲染的本質是裁剪的疊加,導致 masksToBounds 對 layer 以及所有 sublayer 進行二次處理。那么我們只要避免使用 masksToBounds 進行二次處理,而是對所有的 sublayer 進行預處理,就可以只進行“畫家算法”,用一次疊加就完成繪制。
那么可行的實現方法大概有下面幾種:
- 【換資源】直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法需要依賴具體情況,并不通用。
- 【mask】再增加一個和背景色相同的遮罩 mask 覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。
- 【UIBezierPath】用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設置只有內部可見,再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是 layer 的布局一旦改變,貝塞爾曲線都需要手動地重新繪制,所以需要對 frame、color 等進行手動地監聽并重繪。
- 【CoreGraphics】重寫
drawRect:
,用 CoreGraphics 相關方法,在需要應用圓角時進行手動繪制。不過 CoreGraphics 效率也很有限,如果需要多次調用也會有效率問題。
- 對于一些圓角可以創建四個背景顏色弧形的 layer 蓋住四個角,從視覺上制造圓角的效果
- 對于 view 的圓形邊框,如果沒有 backgroundColor,可以放心使用 cornerRadius 來做
- 對于所有的陰影,使用 shadowPath 來規避離屏渲染
- 對于特殊形狀的 view,使用 layer mask 并打開 shouldRasterize 來對渲染結果進行緩存
觸發離屏渲染原因的總結
總結一下,下面幾種情況會觸發離屏渲染:
- 使用了 mask 的 layer (
layer.mask
) - 需要進行裁剪的 layer (
layer.masksToBounds
/view.clipsToBounds
) - 設置了組透明度為 YES,并且透明度不為 1 的 layer (
layer.allowsGroupOpacity
/layer.opacity
) - 添加了陰影的 layer (
layer.shadow
) - 采用了光柵化的 layer (
layer.shouldRasterize
) - 繪制了文字的 layer (
UILabel
,CATextLayer
,Core Text
等)
不過,需要注意的是,重寫 drawRect:
方法并不會觸發離屏渲染。前文中我們提到過,重寫 drawRect:
會將 GPU 中的渲染操作轉移到 CPU 中完成,并且需要額外開辟內存空間。但根據蘋果工程師的說法,這和標準意義上的離屏渲染并不一樣,在 Instrument 中開啟 Color offscreen rendered yellow 調試時也會發現這并不會被判斷為離屏渲染。
圖形處理相關框架
1、iOS與圖形圖像處理相關的框架匯總:
- 界面圖形框架 – UIKit
- 核心動畫框架 – Core Animation
- 蘋果封裝的圖形框架 – Core Graphics & Quartz 2D
- 傳統跨平臺圖形框架 – OpenGL ES
- 蘋果最新力推的圖形框架 – Metal
- 適合圖片的蘋果濾鏡框架 – Core Image
適合視頻的第三方濾鏡方案 -- GPUImage (第三方不屬于系統,這里列出來學習)
- 游戲引擎 – Scene Kit (3D) 和 Sprite Kit (2D)
- 計算機視覺在iOS的應用 – OpenCV for iOS
毫無疑問,開發者們接觸得最多的框架是以下幾個,UIKit、Core Animation,Core Graphic, Core Image。下面簡要介紹這幾個框架,順便介紹下GPUImage
:
2、界面圖形框架 – UIKit(穿插使用其他圖形處理框架)
-
UIKit是一組Objective-C API,為線條圖形、Quartz圖像和顏色操作提供Objective-C 封裝,并提供2D繪制、圖像處理及用戶接口級別的動畫。
-
UIKit包括UIBezierPath(繪制線、角度、橢圓及其它圖形)、UIImage(顯示圖像)、UIColor(顏色操作)、UIFont和UIScreen(提供字體和屏幕信息)等類以及在位圖圖形環境、PDF圖形環境上進行繪制和 操作的功能等, 也提供對標準視圖的支持,也提供對打印功能的支持。
-
UIKit與Core Graphics的關系:
在UIKit中,UIView類本身在繪制時自動創建一個圖形環境,即Core Graphics層的CGContext類型,作為當前的圖形繪制環境。在繪制時可以調用 UIGraphicsGetCurrentContext 函數獲得當前的圖形環境;
3、核心動畫框架 – Core Animation
- Core Animation 是常用的框架之一。它比 UIKit 和 AppKit 更底層。正如我們所知,UIView底下封裝了一層CALayer樹,Core Animation 層是真正的渲染層,我們之所以能在屏幕上看到內容,真正的渲染工作是在 Core Animation 層進行的。
- Core Animation 是一套Objective-C API,實現了一個高性能的復合引擎,并提供一個簡單易用的編程接口,給用戶UI添加平滑運動和動態反饋能力。
- Core Animation 是 UIKit 實現動畫和變換的基礎,也負責視圖的復合功能。使用Core Animation可以實現定制動畫和細粒度的動畫控制,創建復雜的、支持動畫和變換的layered 2D視圖
- OpenGL ES的內容也可以與Core Animation內容進行集成。
- 為了使用Core Animation實現動畫,可以修改 層的屬性值 來觸發一個action對象的執行,不同的action對象實現不同的動畫。Core Animation 提供了一組基類及子類,提供對不同動畫類型的支持:
- CAAnimation 是一個抽象公共基類,CAAnimation采用CAMediaTiming 和CAAction協議為動畫提供時間(如周期、速度、重復次數等)和action行為(啟動、停止等)。
- CAPropertyAnimation 是 CAAnimation的抽象子類,為動畫提供一個由一個key路徑規定的層屬性的支持;
- CABasicAnimation 是CAPropertyAnimation的具體子類,為一個層屬性提供簡單插入能力。
- CAKeyframeAnimation 也是CAPropertyAnimation的具體子類,提供key幀動畫支持。
4、蘋果封裝的圖形框架 – Core Graphics & Quartz 2D
- Core Graphics(使用Quartz 2D引擎)
- Core Graphics是一套C-based API, 支持向量圖形,線、形狀、圖案、路徑、剃度、位圖圖像和pdf 內容的繪制
- Core Graphics 也是常用的框架之一。它用于運行時繪制圖像。開發者們可以通過 Core Graphics 繪制路徑、顏色。當開發者需要在運行時創建圖像時,可以使用 Core Graphics 去繪制,運行時實時計算、繪制一系列圖像幀來實現動畫。與之相對的是運行前創建圖像(例如從磁盤中或內存中已經創建好的UIImage圖像)。
- Quartz 2D
- Quartz 2D是Core Graphics中的2D 繪制呈現引擎。Quartz是資源和設備無關的,提供路徑繪制,anti-aliased呈現,剃度填充圖案,圖像,透明繪制和透明層、遮蔽和陰影、顏色管理,坐標轉換,字體、offscreen呈現、pdf文檔創建、顯示和分析等功能。
- Quartz 2D能夠與所有的圖形和動畫技術(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式進行繪制。
- Quartz 2D提供的主要類包括:
- CGContext:表示一個圖形環境;
- CGPath:使用向量圖形來創建路徑,并能夠填充和stroke;
- CGImage:用來表示位圖;
- CGLayer:用來表示一個能夠用于重復繪制和offscreen繪制的繪制層;
- CGPattern:用來表示Pattern,用于重復繪制;
- CGShading和 CGGradient:用于繪制剃度;
- CGColor 和 CGColorSpace;用來進行顏色和顏色空間管理;
- CGFont, 用于繪制文本;
- CGPDFContentStream、CGPDFScanner、CGPDFPage、CGPDFObject,CGPDFStream, CGPDFString等用來進行pdf文件的創建、解析和顯示。
5、適合圖片的蘋果濾鏡框架 – Core Image
- Core Image 與 Core Graphics 恰恰相反,Core Graphics 用于在運行時創建圖像,而 Core Image 是用來處理已經創建的圖像的。Core Image 框架擁有一系列現成的圖像過濾器,能對已存在的圖像進行高效的處理。
- Core Image 是 iOS5 新加入到 iOS 平臺的一個圖像處理框架,提供了強大高效的圖像處理功能, 用來對基于像素的圖像進行操作與分析, 內置了很多強大的濾鏡(Filter) (目前數量超過了180種), 這些Filter 提供了各種各樣的效果, 并且還可以通過 濾鏡鏈 將各種效果的 Filter疊加 起來形成強大的自定義效果。
- 一個 濾鏡 是一個對象,有很多輸入和輸出,并執行一些變換。例如,模糊濾鏡可能需要輸入圖像和一個模糊半徑來產生適當的模糊后的輸出圖像。
- 一個 濾鏡鏈 是一個鏈接在一起的濾鏡網絡,使得一個濾鏡的輸出可以是另一個濾鏡的輸入。以這種方式,可以實現精心制作的效果。
- iOS8 之后更是支持自定義 CIFilter,可以定制滿足業務需求的復雜效果。
- Core Image 的優點在于十分高效。大部分情況下,它會在 GPU 中完成工作,但如果 GPU 忙,會使用 CPU 進行處理。如果設備支持 Metal,那么會使用 Metal 處理。這些操作會在底層完成,Apple 的工程師們已經幫助開發者們完成這些操作了。
- Core Image 的 API 主要就是三類:
- CIImage 保存圖像數據的類,可以通過UIImage,圖像文件或者像素數據來創建,包括未處理的像素數據。
- CIFilter 表示應用的濾鏡,這個框架中對圖片屬性進行細節處理的類。它對所有的像素進行操作,用一些鍵-值設置來決定具體操作的程度。
- CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于處理繪制渲染以及處理托管對象一樣,Core Image 的上下文也是實現對圖像處理的具體對象。可以從其中取得圖片的信息。
6、適合視頻的第三方濾鏡方案 – GPUImage
-
GPUImage
是一個基于OpenGL ES 2.0的開源的圖像處理庫,優勢:
- 最低支持 iOS 4.0,iOS 5.0 之后就支持自定義濾鏡。在低端機型上,GPUImage 有更好的表現。
- GPUImage 在視頻處理上有更好的表現。
- GPUImage 的代碼已經開源。可以根據自己的業務需求,定制更加復雜的管線操作。可定制程度高。
參考文章:
iOS - 圖形高級處理 (一、圖片顯示相關理論)
iOS Rendering 渲染全解析(長文干貨)
關于 iOS 離屏渲染的分析與處理
iOS-圖片高級處理(三、圖片處理實踐)