iOS Rendering 渲染全解析(長文乾貨)

但願經過這篇文章從頭至尾梳理一下 iOS 中涉及到渲染原理相關的內容,會先從計算機渲染原理講起,慢慢說道 iOS 的渲染原理和框架,最後再深刻探討一下離屏渲染。ios

但願能對你們有點幫助~面試

 

1. 計算機渲染原理

CPU 與 GPU 的架構

對於現代計算機系統,簡單來講能夠大概視做三層架構:硬件、操做系統與進程。對於移動端來講,進程就是 app,而 CPU 與 GPU 是硬件層面的重要組成部分。CPU 與 GPU 提供了計算能力,經過操做系統被 app 調用。算法

 
  • CPU(Central Processing Unit):現代計算機整個系統的運算核心、控制核心。
  • GPU(Graphics Processing Unit):可進行繪圖運算工做的專用微處理器,是鏈接計算機和顯示終端的紐帶。

CPU 和 GPU 其設計目標就是不一樣的,它們分別針對了兩種不一樣的應用場景。CPU 是運算核心與控制核心,須要有很強的運算通用性,兼容各類數據類型,同時也須要能處理大量不一樣的跳轉、中斷等指令,所以 CPU 的內部結構更爲複雜。而 GPU 則面對的是類型統1、更加單純的運算,也不須要處理複雜的指令,但也肩負着更大的運算任務。緩存

 

所以,CPU 與 GPU 的架構也不一樣。由於 CPU 面臨的狀況更加複雜,所以從上圖中也能夠看出,CPU 擁有更多的緩存空間 Cache 以及複雜的控制單元,計算能力並非 CPU 的主要訴求。CPU 是設計目標是低時延,更多的高速緩存也意味着能夠更快地訪問數據;同時複雜的控制單元也能更快速地處理邏輯分支,更適合串行計算。數據結構

而 GPU 擁有更多的計算單元 Arithmetic Logic Unit,具備更強的計算能力,同時也具備更多的控制單元。GPU 基於大吞吐量而設計,每一部分緩存都鏈接着一個流處理器(stream processor),更加適合大規模的並行計算。架構

圖像渲染流水線

圖像渲染流程粗粒度地大概分爲下面這些步驟:併發

 

上述圖像渲染流水線中,除了第一部分 Application 階段,後續主要都由 GPU 負責,爲了方便後文講解,先將 GPU 的渲染流程圖展現出來:app

 

上圖就是一個三角形被渲染的過程當中,GPU 所負責的渲染流水線。能夠看到簡單的三角形繪製就須要大量的計算,若是再有更多更復雜的頂點、顏色、紋理信息(包括 3D 紋理),那麼計算量是不可思議的。這也是爲何 GPU 更適合於渲染流程。框架

接下來,具體講解渲染流水線中各個部分的具體任務:iview

Application 應用處理階段:獲得圖元

這個階段具體指的就是圖像在應用中被處理的階段,此時還處於 CPU 負責的時期。在這個階段應用可能會對圖像進行一系列的操做或者改變,最終將新的圖像信息傳給下一階段。這部分信息被叫作圖元(primitives),一般是三角形、線段、頂點等。

Geometry 幾何處理階段:處理圖元

進入這個階段以後,以及以後的階段,就都主要由 GPU 負責了。此時 GPU 能夠拿到上一個階段傳遞下來的圖元信息,GPU 會對這部分圖元進行處理,以後輸出新的圖元。這一系列階段包括:

  • 頂點着色器(Vertex Shader):這個階段中會將圖元中的頂點信息進行視角轉換、添加光照信息、增長紋理等操做。
  • 形狀裝配(Shape Assembly):圖元中的三角形、線段、點分別對應三個 Vertex、兩個 Vertex、一個 Vertex。這個階段會將 Vertex 鏈接成相對應的形狀。
  • 幾何着色器(Geometry Shader):額外添加額外的Vertex,將原始圖元轉換成新圖元,以構建一個不同的模型。簡單來講就是基於經過三角形、線段和點構建更復雜的幾何圖形。

Rasterization 光柵化階段:圖元轉換爲像素

光柵化的主要目的是將幾何渲染以後的圖元信息,轉換爲一系列的像素,以便後續顯示在屏幕上。這個階段中會根據圖元信息,計算出每一個圖元所覆蓋的像素信息等,從而將像素劃分紅不一樣的部分。

 

一種簡單的劃分就是根據中心點,若是像素的中心點在圖元內部,那麼這個像素就屬於這個圖元。如上圖所示,深藍色的線就是圖元信息所構建出的三角形;而經過是否覆蓋中心點,能夠遍歷出全部屬於該圖元的全部像素,即淺藍色部分。

Pixel 像素處理階段:處理像素,獲得位圖

通過上述光柵化階段,咱們獲得了圖元所對應的像素,此時,咱們須要給這些像素填充顏色和效果。因此最後這個階段就是給像素填充正確的內容,最終顯示在屏幕上。這些通過處理、蘊含大量信息的像素點集合,被稱做位圖(bitmap)。也就是說,Pixel 階段最終輸出的結果就是位圖,過程具體包含:

這些點能夠進行不一樣的排列和染色以構成圖樣。當放大位圖時,能夠看見賴以構成整個圖像的無數單個方塊。只要有足夠多的不一樣色彩的像素,就能夠製做出色彩豐富的圖象,逼真地表現天然界的景象。縮放和旋轉容易失真,同時文件容量較大。

  • 片斷着色器(Fragment Shader):也叫作 Pixel Shader,這個階段的目的是給每個像素 Pixel 賦予正確的顏色。顏色的來源就是以前獲得的頂點、紋理、光照等信息。因爲須要處理紋理、光照等複雜信息,因此這一般是整個系統的性能瓶頸。
  • 測試與混合(Tests and Blending):也叫作 Merging 階段,這個階段主要處理片斷的先後位置以及透明度。這個階段會檢測各個着色片斷的深度值 z 座標,從而判斷片斷的先後位置,以及是否應該被捨棄。同時也會計算相應的透明度 alpha 值,從而進行片斷的混合,獲得最終的顏色。

2. 屏幕成像與卡頓

在圖像渲染流程結束以後,接下來就須要將獲得的像素信息顯示在物理屏幕上了。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 渲染性能,減小掉幀次數。

3. 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 的能力,實現硬件加速渲染。OpenGL的高效實現(利用了圖形加速硬件)通常由顯示設備廠商提供,並且很是依賴於該廠商提供的硬件。OpenGL 之上擴展出不少東西,如 Core Graphics 等最終都依賴於 OpenGL,有些狀況下爲了更高的效率,好比遊戲程序,甚至會直接調用 OpenGL 的接口。

Core Graphics:Core Graphics 是一個強大的二維圖像繪製引擎,是 iOS 的核心圖形庫,經常使用的好比 CGRect 就定義在這個框架下。

Core Animation:在 iOS 上,幾乎全部的東西都是經過 Core Animation 繪製出來,它的自由度更高,使用範圍也更廣。

Core Image:Core Image 是一個高性能的圖像處理分析的框架,它擁有一系列現成的圖像濾鏡,能對已存在的圖像進行高效的處理。

Metal:Metal 相似於 OpenGL ES,也是一套第三方標準,具體實現由蘋果實現。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構建於 Metal 之上的。

Core Animation 是什麼

Render, compose, and animate visual elements. ---- Apple

Core Animation,它本質上能夠理解爲一個複合引擎,主要職責包含:渲染、構建和實現動畫。

一般咱們會使用 Core Animation 來高效、方便地實現動畫,可是實際上它的前身叫作 Layer Kit,關於動畫實現只是它功能中的一部分。對於 iOS app,不管是否直接使用了 Core Animation,它都在底層深度參與了 app 的構建。而對於 OS X app,也能夠經過使用 Core Animation 方便地實現部分功能。

 

Core Animation 是 AppKit 和 UIKit 完美的底層支持,同時也被整合進入 Cocoa 和 Cocoa Touch 的工做流之中,它是 app 界面渲染和構建的最基礎架構。 Core Animation 的職責就是儘量快地組合屏幕上不一樣的可視內容,這個內容是被分解成獨立的 layer(iOS 中具體而言就是 CALayer),而且被存儲爲樹狀層級結構。這個樹也造成了 UIKit 以及在 iOS 應用程序當中你所能在屏幕上看見的一切的基礎。

簡單來講就是用戶能看到的屏幕上的內容都由 CALayer 進行管理。那麼 CALayer 到底是如何進行管理的呢?另外在 iOS 開發過程當中,最大量使用的視圖控件其實是 UIView 而不是 CALayer,那麼他們二者的關係到底如何呢?

CALayer 是顯示的基礎:存儲 bitmap

簡單理解,CALayer 就是屏幕顯示的基礎。那 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 的內容,是一個指針類型,在 iOS 中的類型就是 CGImageRef(在 OS X 中還能夠是 NSImage)。而咱們進一步查到,Apple 對 CGImageRef 的定義是:

A bitmap image or image mask.

看到 bitmap,這下咱們就能夠和以前講的的渲染流水線聯繫起來了:實際上,CALayer 中的 contents 屬性保存了由設備渲染流水線渲染好的位圖 bitmap(一般也被稱爲 backing store),而當設備屏幕進行刷新時,會從 CALayer 中讀取生成好的 bitmap,進而呈現到屏幕上。

因此,若是咱們在代碼中對 CALayer 的 contents 屬性進行了設置,好比這樣:

// 注意 CGImage 和 CGImageRef 的關係:
// typedef struct CGImage CGImageRef;
layer.contents = (__bridge id)image.CGImage;

那麼在運行時,操做系統會調用底層的接口,將 image 經過 CPU+GPU 的渲染流水線渲染獲得對應的 bitmap,存儲於 CALayer.contents 中,在設備屏幕進行刷新的時候就會讀取 bitmap 在屏幕上呈現。

也正由於每次要被渲染的內容是被靜態的存儲起來的,因此每次渲染時,Core Animation 會觸發調用 drawRect: 方法,使用存儲好的 bitmap 進行新一輪的展現。

CALayer 與 UIView 的關係

UIView 做爲最經常使用的視圖控件,和 CALayer 也有着千絲萬縷的聯繫,那麼二者之間究竟是個什麼關係,他們有什麼差別?

固然,二者有不少顯性的區別,好比是否可以響應點擊事件。但爲了從根本上完全搞懂這些問題,咱們必需要先搞清楚二者的職責。

UIView - Apple

Views are the fundamental building blocks of your app's user interface, and the UIView class 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.

根據 Apple 的官方文檔,UIView 是 app 中的基本組成結構,定義了一些統一的規範。它會負責內容的渲染以及,處理交互事件。具體而言,它負責的事情能夠歸爲下面三類

  • 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 的主要職責是管理內部的可視內容,這也和咱們前文所講的內容吻合。當咱們建立一個 UIView 的時候,UIView 會自動建立一個 CALayer,爲自身提供存儲 bitmap 的地方(也就是前文說的 backing store),並將自身固定設置爲 CALayer 的代理。

 

從這兒咱們大概總結出下面兩個核心關係:

  1. CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
  2. UIView 提供了對 CALayer 部分功能的封裝,同時也另外負責了交互事件的處理。

有了這兩個最關鍵的根本關係,那麼下面這些常常出如今面試答案裏的顯性的異同就很好解釋了。舉幾個例子:

  • 相同的層級結構:咱們對 UIView 的層級結構很是熟悉,因爲每一個 UIView 都對應 CALayer 負責頁面的繪製,因此 CALayer 也具備相應的層級結構。

  • 部分效果的設置:由於 UIView 只對 CALayer 的部分功能進行了封裝,而另外一部分如圓角、陰影、邊框等特效都須要經過調用 layer 屬性來設置。

  • 是否響應點擊事件:CALayer 不負責點擊事件,因此不響應點擊事件,而 UIView 會響應。

  • 不一樣繼承關係:CALayer 繼承自 NSObject,UIView 因爲要負責交互事件,因此繼承自 UIResponder。

固然還剩最後一個問題,爲何要將 CALayer 獨立出來,直接使用 UIView 統一管理不行嗎?爲何不用一個統一的對象來處理全部事情呢?

這樣設計的主要緣由就是爲了職責分離,拆分功能,方便代碼的複用。經過 Core Animation 框架來負責可視內容的呈現,這樣在 iOS 和 OS X 上均可以使用 Core Animation 進行渲染。與此同時,兩個系統還能夠根據交互規則的不一樣來進一步封裝統一的控件,好比 iOS 有 UIKit 和 UIView,OS X 則是AppKit 和 NSView。

4. Core Animation 渲染全內容

Core Animation Pipeline 渲染流水線

當咱們瞭解了 Core Animation 以及 CALayer 的基本知識後,接下來咱們來看下 Core Animation 的渲染流水線。

 

整個流水線一共有下面幾個步驟:

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:構建視圖

這個階段主要處理視圖的構建和佈局,具體步驟包括:

  1. 調用重載的 layoutSubviews 方法
  2. 建立視圖,並經過 addSubview 方法添加子視圖
  3. 計算視圖佈局,即全部的 Layout Constraint

因爲這個階段是在 CPU 中進行,一般是 CPU 限制或者 IO 限制,因此咱們應該儘可能高效輕量地操做,減小這部分的時間,好比減小非必要的視圖建立、簡化佈局計算、減小視圖層級等。

Display:繪製視圖

這個階段主要是交給 Core Graphics 進行視圖的繪製,注意不是真正的顯示,而是獲得前文所說的圖元 primitives 數據:

  1. 根據上一階段 Layout 的結果建立獲得圖元信息。
  2. 若是重寫了 drawRect: 方法,那麼會調用重載的 drawRect: 方法,在 drawRect: 方法中手動繪製獲得 bitmap 數據,從而自定義視圖的繪製。

注意正常狀況下 Display 階段只會獲得圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據圖元信息繪製獲得的。可是若是重寫了 drawRect: 方法,這個方法會直接調用 Core Graphics 繪製方法獲得 bitmap 數據,同時系統會額外申請一塊內存,用於暫存繪製好的 bitmap。

因爲重寫了 drawRect: 方法,致使繪製過程從 GPU 轉移到了 CPU,這就致使了必定的效率損失。與此同時,這個過程會額外使用 CPU 和內存,所以須要高效繪製,不然容易形成 CPU 卡頓或者內存爆炸。

Prepare:Core Animation 額外的工做

這一步主要是:圖片解碼和轉換

Commit:打包併發送

這一步主要是:圖層打包併發送到 Render Server。

注意 commit 操做是依賴圖層樹遞歸執行的,因此若是圖層樹過於複雜,commit 的開銷就會很大。這也是咱們但願減小視圖層級,從而下降圖層樹複雜度的緣由。

Rendering Pass: Render Server 的具體操做

 

Render Server 一般是 OpenGL 或者是 Metal。以 OpenGL 爲例,那麼上圖主要是 GPU 中執行的操做,具體主要包括:

  1. GPU 收到 Command Buffer,包含圖元 primitives 信息
  2. Tiler 開始工做:先經過頂點着色器 Vertex Shader 對頂點進行處理,更新圖元信息
  3. 平鋪過程:平鋪生成 tile bucket 的幾何圖形,這一步會將圖元信息轉化爲像素,以後將結果寫入 Parameter Buffer 中
  4. Tiler 更新完全部的圖元信息,或者 Parameter Buffer 已滿,則會開始下一步
  5. Renderer 工做:將像素信息進行處理獲得 bitmap,以後存入 Render Buffer
  6. Render Buffer 中存儲有渲染好的 bitmap,供以後的 Display 操做使用

使用 Instrument 的 OpenGL ES,能夠對過程進行監控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 能夠分別監控 Tiler 和 Renderer 的工做狀況

5. Offscreen Rendering 離屏渲染

離屏渲染做爲一個面試高頻問題,時常被說起,下面來從頭至尾講一下離屏渲染。

離屏渲染具體過程

根據前文,簡化來看,一般的渲染流程是這樣的:

 

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 倍。

可見離屏渲染的開銷很是大,一旦須要離屏渲染的內容過多,很容易形成掉幀的問題。因此大部分狀況下,咱們都應該儘可能避免離屏渲染。

爲何使用離屏渲染

那麼爲何要使用離屏渲染呢?主要是由於下面這兩種緣由:

  1. 一些特殊效果須要使用額外的 Offscreen Buffer 來保存渲染的中間狀態,因此不得不使用離屏渲染。
  2. 處於效率目的,能夠將內容提早渲染保存在 Offscreen Buffer 中,達到複用的目的。

對於第一種狀況,也就是不得不使用離屏渲染的狀況,通常都是系統自動觸發的,好比陰影、圓角等等。

最多見的情形之一就是:使用了 mask 蒙版。

 

如圖所示,因爲最終的內容是由兩層渲染結果疊加,因此必需要利用額外的內存空間對中間的渲染結果進行保存,所以系統會默認觸發離屏渲染。

又好比下面這個例子,iOS 8 開始提供的模糊特效 UIBlurEffectView:

 

整個模糊過程分爲多步:Pass 1 先渲染須要模糊的內容自己,Pass 2 對內容進行縮放,Pass 3 4 分別對上一步內容進行橫縱方向的模糊操做,最後一步用模糊後的結果疊加合成,最終實現完整的模糊特效。

而第二種狀況,爲了複用提升效率而使用離屏渲染通常是主動的行爲,是經過 CALayer 的 shouldRasterize 光柵化操做實現的。

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.

開啓光柵化後,會觸發離屏渲染,Render Server 會強制將 CALayer 的渲染位圖結果 bitmap 保存下來,這樣下次再須要渲染時就能夠直接複用,從而提升效率。

而保存的 bitmap 包含 layer 的 subLayer、圓角、陰影、組透明度 group opacity 等,因此若是 layer 的構成包含上述幾種元素,結構複雜且須要反覆利用,那麼就能夠考慮打開光柵化。

圓角、陰影、組透明度等會由系統自動觸發離屏渲染,那麼打開光柵化能夠節約第二次及之後的渲染時間。而多層 subLayer 的狀況因爲不會自動觸發離屏渲染,因此相比之下會多花費第一次離屏渲染的時間,可是能夠節約後續的重複渲染的開銷。

不過使用光柵化的時候須要注意如下幾點:

  1. 若是 layer 不能被複用,則沒有必要打開光柵化
  2. 若是 layer 不是靜態,須要被頻繁修改,好比處於動畫之中,那麼開啓離屏渲染反而影響效率
  3. 離屏渲染緩存內容有時間限制,緩存內容 100ms 內若是沒有被使用,那麼就會被丟棄,沒法進行復用
  4. 離屏渲染緩存空間有限,超過 2.5 倍屏幕像素大小的話也會失效,沒法複用

圓角的離屏渲染

一般來說,設置了 layer 的圓角效果以後,會自動觸發離屏渲染。可是究竟什麼狀況下設置圓角纔會觸發離屏渲染呢?

 

如上圖所示,layer 由三層組成,咱們設置圓角一般會首先像下面這行代碼同樣進行設置:

view.layer.cornerRadius = 2

根據 cornerRadius - Apple 的描述,上述代碼只會默認設置 backgroundColor 和 border 的圓角,而不會設置 content 的圓角,除非同時設置了 layer.masksToBounds 爲 true(對應 UIView 的 clipsToBounds 屬性):

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

若是隻是設置了 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 進行預處理,就能夠只進行「畫家算法」,用一次疊加就完成繪製。

那麼可行的實現方法大概有下面幾種:

  1. 【換資源】直接使用帶圓角的圖片,或者替換背景色爲帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法須要依賴具體狀況,並不通用。
  2. 【mask】再增長一個和背景色相同的遮罩 mask 覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色爲圖片或漸變色的狀況。
  3. 【UIBezierPath】用貝塞爾曲線繪製閉合帶圓角的矩形,在上下文中設置只有內部可見,再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,可是 layer 的佈局一旦改變,貝塞爾曲線都須要手動地從新繪製,因此須要對 frame、color 等進行手動地監聽並重繪。
  4. 【CoreGraphics】重寫 drawRect:,用 CoreGraphics 相關方法,在須要應用圓角時進行手動繪製。不過 CoreGraphics 效率也頗有限,若是須要屢次調用也會有效率問題。

觸發離屏渲染緣由的總結

總結一下,下面幾種狀況會觸發離屏渲染:

  1. 使用了 mask 的 layer (layer.mask)
  2. 須要進行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
  3. 設置了組透明度爲 YES,而且透明度不爲 1 的 layer (layer.allowsGroupOpacity/layer.opacity)
  4. 添加了投影的 layer (layer.shadow*)
  5. 採用了光柵化的 layer (layer.shouldRasterize)
  6. 繪製了文字的 layer (UILabelCATextLayerCore Text 等)

不過,須要注意的是,重寫 drawRect: 方法並不會觸發離屏渲染。前文中咱們提到過,重寫 drawRect: 會將 GPU 中的渲染操做轉移到 CPU 中完成,而且須要額外開闢內存空間。但根據蘋果工程師的說法,這和標準意義上的離屏渲染並不同,在 Instrument 中開啓 Color offscreen rendered yellow 調試時也會發現這並不會被判斷爲離屏渲染。

6. 自測題目

通常來講作點題才能加深理解和鞏固,因此這裏從文章裏簡單提煉了一些,但願能幫到你們:

  1. CPU 和 GPU 的設計目的分別是什麼?
  2. CPU 和 GPU 哪一個的 Cache\ALU\Control unit 的比例更高?
  3. 計算機圖像渲染流水線的大體流程是什麼?
  4. Framebuffer 幀緩衝器的做用是什麼?
  5. Screen Tearing 屏幕撕裂是怎麼形成的?
  6. 如何解決屏幕撕裂的問題?
  7. 掉幀是怎麼產生的?
  8. CoreAnimation 的職責是什麼?
  9. UIView 和 CALayer 是什麼關係?有什麼區別?
  10. 爲何會同時有 UIView 和 CALayer,可否合成一個?
  11. 渲染流水線中,CPU 會負責哪些任務?
  12. 離屏渲染爲何會有效率問題?
  13. 何時應該使用離屏渲染?
  14. shouldRasterize 光柵化是什麼?
  15. 有哪些常見的觸發離屏渲染的狀況?
  16. cornerRadius 設置圓角會觸發離屏渲染嗎?
  17. 圓角觸發的離屏渲染有哪些解決方案?
  18. 重寫 drawRect 方法會觸發離屏渲染嗎?

推薦👇:

  • 020 持續更新,精品小圈子每日都有新內容,乾貨濃度極高。

  • 結實人脈、討論技術 你想要的這裏都有!

  • 搶先入羣,跑贏同齡人!(入羣無需任何費用)

  • (直接搜索羣號:789143298,快速入羣)
  • 點擊此處,與iOS開發大牛一塊兒交流學習

申請即送:

  • BAT大廠面試題、獨家面試工具包,

  • 資料免費領取,包括 數據結構、底層進階、圖形視覺、音視頻、架構設計、逆向安防、RxSwift、flutter,

     
相關文章
相關標籤/搜索