您好,登錄后才能下訂單哦!
這篇文章主要講解了“iOS渲染原理是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“iOS渲染原理是什么”吧!
cdn.xitu.io/2020/6/2/1727430740ce99d7?w=515&h=405&f=png&s=73957">
對于現代計算機系統,簡單來說可以大概視作三層架構:硬件、操作系統與進程。對于移動端來說,進程就是 app,而 CPU 與 GPU 是硬件層面的重要組成部分。CPU 與 GPU 提供了計算能力,通過操作系統被 app 調用。
CPU(Central Processing Unit):現代計算機整個系統的運算核心、控制核心。
GPU(Graphics Processing Unit):可進行繪圖運算工作的專用微處理器,是連接計算機和顯示終端的紐帶。
CPU 和 GPU 其設計目標就是不同的,它們分別針對了兩種不同的應用場景。CPU 是運算核心與控制核心,需要有很強的運算通用性,兼容各種數據類型,同時也需要能處理大量不同的跳轉、中斷等指令,因此 CPU 的內部結構更為復雜。而 GPU 則面對的是類型統一、更加單純的運算,也不需要處理復雜的指令,但也肩負著更大的運算任務。
因此,CPU 與 GPU 的架構也不同。因為 CPU 面臨的情況更加復雜,因此從上圖中也可以看出,CPU 擁有更多的緩存空間 Cache 以及復雜的控制單元,計算能力并不是 CPU 的主要訴求。CPU 是設計目標是低時延,更多的高速緩存也意味著可以更快地訪問數據;同時復雜的控制單元也能更快速地處理邏輯分支,更適合串行計算。
而 GPU 擁有更多的計算單元 Arithmetic Logic Unit,具有更強的計算能力,同時也具有更多的控制單元。GPU 基于大吞吐量而設計,每一部分緩存都連接著一個流處理器(stream processor),更加適合大規模的并行計算。
圖像渲染流程粗粒度地大概分為下面這些步驟:
上述圖像渲染流水線中,除了第一部分 Application 階段,后續主要都由 GPU 負責,為了方便后文講解,先將 GPU 的渲染流程圖展示出來:
上圖就是一個三角形被渲染的過程中,GPU 所負責的渲染流水線。可以看到簡單的三角形繪制就需要大量的計算,如果再有更多更復雜的頂點、顏色、紋理信息(包括 3D 紋理),那么計算量是難以想象的。這也是為什么 GPU 更適合于渲染流程。
接下來,具體講解渲染流水線中各個部分的具體任務:
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 值,從而進行片段的混合,得到最終的顏色。
在圖像渲染流程結束之后,接下來就需要將得到的像素信息顯示在物理屏幕上了。GPU 最后一步渲染結束之后像素信息,被存在幀緩沖器(Framebuffer)中,之后視頻控制器(Video Controller)會讀取幀緩沖器中的信息,經過數模轉換傳遞給顯示器(Monitor),進行顯示。完整的流程如下圖所示:
經過 GPU 處理之后的像素集合,也就是位圖,會被幀緩沖器緩存起來,供之后的顯示使用。顯示器的電子束會從屏幕的左上角開始逐行掃描,屏幕上的每個點的圖像信息都從幀緩沖器中的位圖進行讀取,在屏幕上對應地顯示。掃描的流程如下圖所示:
電子束掃描的過程中,屏幕就能呈現出對應的結果,每次整個屏幕被掃描完一次后,就相當于呈現了一幀完整的圖像。屏幕不斷地刷新,不停呈現新的幀,就能呈現出連續的影像。而這個屏幕刷新的頻率,就是幀率(Frame per Second,FPS)。由于人眼的視覺暫留效應,當屏幕刷新頻率足夠高時(FPS 通常是 50 到 60 左右),就能讓畫面看起來是連續而流暢的。對于 iOS 而言,app 應該盡量保證 60 FPS 才是最好的體驗。
在這種單一緩存的模式下,最理想的情況就是一個流暢的流水線:每次電子束從頭開始新的一幀的掃描時,CPU+GPU 對于該幀的渲染流程已經結束,渲染好的位圖已經放入幀緩沖器中。但這種完美的情況是非常脆弱的,很容易產生屏幕撕裂:
CPU+GPU 的渲染流程是一個非常耗時的過程。如果在電子束開始掃描新的一幀時,位圖還沒有渲染好,而是在掃描到屏幕中間時才渲染完成,被放入幀緩沖器中 —— 那么已掃描的部分就是上一幀的畫面,而未掃描的部分則會顯示新的一幀圖像,這就造成屏幕撕裂。
解決屏幕撕裂、提高顯示效率的一個策略就是使用垂直同步信號 Vsync 與雙緩沖機制 Double Buffering。根據蘋果的官方文檔描述,iOS 設備會始終使用 Vsync + Double Buffering 的策略。
垂直同步信號(vertical synchronisation,Vsync)相當于給幀緩沖器加鎖:當電子束完成一幀的掃描,將要從頭開始掃描時,就會發出一個垂直同步信號。只有當視頻控制器接收到 Vsync 之后,才會將幀緩沖器中的位圖更新為下一幀,這樣就能保證每次顯示的都是同一幀的畫面,因而避免了屏幕撕裂。
但是這種情況下,視頻控制器在接受到 Vsync 之后,就要將下一幀的位圖傳入,這意味著整個 CPU+GPU 的渲染流程都要在一瞬間完成,這是明顯不現實的。所以雙緩沖機制會增加一個新的備用緩沖器(back buffer)。渲染結果會預先保存在 back buffer 中,在接收到 Vsync 信號的時候,視頻控制器會將 back buffer 中的內容置換到 frame buffer 中,此時就能保證置換操作幾乎在一瞬間完成(實際上是交換了內存地址)。
啟用 Vsync 信號以及雙緩沖機制之后,能夠解決屏幕撕裂的問題,但是會引入新的問題:掉幀。如果在接收到 Vsync 之時 CPU 和 GPU 還沒有渲染好新的位圖,視頻控制器就不會去替換 frame buffer 中的位圖。這時屏幕就會重新掃描呈現出上一幀一模一樣的畫面。相當于兩個周期顯示了同樣的畫面,這就是所謂掉幀的情況。
如圖所示,A、B 代表兩個幀緩沖器,當 B 沒有渲染完畢時就接收到了 Vsync 信號,所以屏幕只能再顯示相同幀 A,這就發生了第一次的掉幀。
事實上上述策略還有優化空間。我們注意到在發生掉幀的時候,CPU 和 GPU 有一段時間處于閑置狀態:當 A 的內容正在被掃描顯示在屏幕上,而 B 的內容已經被渲染好,此時 CPU 和 GPU 就處于閑置狀態。那么如果我們增加一個幀緩沖器,就可以利用這段時間進行下一步的渲染,并將渲染結果暫存于新增的幀緩沖器中。
如圖所示,由于增加了新的幀緩沖器,可以一定程度上地利用掉幀的空檔期,合理利用 CPU 和 GPU 性能,從而減少掉幀的次數。
手機使用卡頓的直接原因,就是掉幀。前文也說過,屏幕刷新頻率必須要足夠高才能流暢。對于 iPhone 手機來說,屏幕最大的刷新頻率是 60 FPS,一般只要保證 50 FPS 就已經是較好的體驗了。但是如果掉幀過多,導致刷新頻率過低,就會造成不流暢的使用體驗。
這樣看來,可以大概總結一下
屏幕卡頓的根本原因:CPU 和 GPU 渲染流水線耗時過長,導致掉幀。
Vsync 與雙緩沖的意義:強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。
三緩沖的意義:合理使用 CPU、GPU 渲染性能,減少掉幀次數。
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 之上的。
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 就是屏幕顯示的基礎。那 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 進行新一輪的展示。
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 的代理。
從這兒我們大概總結出下面兩個核心關系:
CALayer 是 UIView 的屬性之一,負責渲染和動畫,提供可視內容的呈現。
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。
當我們了解了 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 觸發顯示。
一般開發當中能影響到的就是 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:打包并發送
這一步主要是:圖層打包并發送到 Render Server。
注意 commit 操作是依賴圖層樹遞歸執行的,所以如果圖層樹過于復雜,commit 的開銷就會很大。這也是我們希望減少視圖層級,從而降低圖層樹復雜度的原因。
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 操作使用
使用 Instrument 的 OpenGL ES,可以對過程進行監控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分別監控 Tiler 和 Renderer 的工作情況
離屏渲染作為一個面試高頻問題,時常被提及,下面來從頭到尾講一下離屏渲染。
根據前文,簡化來看,通常的渲染流程是這樣的:
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 光柵化操作實現的。
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 的情況由于不會自動觸發離屏渲染,所以相比之下會多花費第一次離屏渲染的時間,但是可以節約后續的重復渲染的開銷。
不過使用光柵化的時候需要注意以下幾點:
如果 layer 不能被復用,則沒有必要打開光柵化
如果 layer 不是靜態,需要被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率
離屏渲染緩存內容有時間限制,緩存內容 100ms 內如果沒有被使用,那么就會被丟棄,無法進行復用
離屏渲染緩存空間有限,超過 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’scontents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property totrue
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 進行預處理,就可以只進行“畫家算法”,用一次疊加就完成繪制。
那么可行的實現方法大概有下面幾種:
【換資源】直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法需要依賴具體情況,并不通用。
【mask】再增加一個和背景色相同的遮罩 mask 覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。
【UIBezierPath】用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設置只有內部可見,再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是 layer 的布局一旦改變,貝塞爾曲線都需要手動地重新繪制,所以需要對 frame、color 等進行手動地監聽并重繪。
【CoreGraphics】重寫
drawRect:
,用 CoreGraphics 相關方法,在需要應用圓角時進行手動繪制。不過 CoreGraphics 效率也很有限,如果需要多次調用也會有效率問題。
感謝各位的閱讀,以上就是“iOS渲染原理是什么”的內容了,經過本文的學習后,相信大家對iOS渲染原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。