您好,登錄后才能下訂單哦!
這篇文章主要講解了“iOS渲染原理是什么”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“iOS渲染原理是什么”吧!
cdn.xitu.io/2020/6/2/1727430740ce99d7?w=515&h=405&f=png&s=73957">
對(duì)于現(xiàn)代計(jì)算機(jī)系統(tǒng),簡(jiǎn)單來(lái)說(shuō)可以大概視作三層架構(gòu):硬件、操作系統(tǒng)與進(jìn)程。對(duì)于移動(dòng)端來(lái)說(shuō),進(jìn)程就是 app,而 CPU 與 GPU 是硬件層面的重要組成部分。CPU 與 GPU 提供了計(jì)算能力,通過(guò)操作系統(tǒng)被 app 調(diào)用。
CPU(Central Processing Unit):現(xiàn)代計(jì)算機(jī)整個(gè)系統(tǒng)的運(yùn)算核心、控制核心。
GPU(Graphics Processing Unit):可進(jìn)行繪圖運(yùn)算工作的專(zhuān)用微處理器,是連接計(jì)算機(jī)和顯示終端的紐帶。
CPU 和 GPU 其設(shè)計(jì)目標(biāo)就是不同的,它們分別針對(duì)了兩種不同的應(yīng)用場(chǎng)景。CPU 是運(yùn)算核心與控制核心,需要有很強(qiáng)的運(yùn)算通用性,兼容各種數(shù)據(jù)類(lèi)型,同時(shí)也需要能處理大量不同的跳轉(zhuǎn)、中斷等指令,因此 CPU 的內(nèi)部結(jié)構(gòu)更為復(fù)雜。而 GPU 則面對(duì)的是類(lèi)型統(tǒng)一、更加單純的運(yùn)算,也不需要處理復(fù)雜的指令,但也肩負(fù)著更大的運(yùn)算任務(wù)。
因此,CPU 與 GPU 的架構(gòu)也不同。因?yàn)?CPU 面臨的情況更加復(fù)雜,因此從上圖中也可以看出,CPU 擁有更多的緩存空間 Cache 以及復(fù)雜的控制單元,計(jì)算能力并不是 CPU 的主要訴求。CPU 是設(shè)計(jì)目標(biāo)是低時(shí)延,更多的高速緩存也意味著可以更快地訪問(wèn)數(shù)據(jù);同時(shí)復(fù)雜的控制單元也能更快速地處理邏輯分支,更適合串行計(jì)算。
而 GPU 擁有更多的計(jì)算單元 Arithmetic Logic Unit,具有更強(qiáng)的計(jì)算能力,同時(shí)也具有更多的控制單元。GPU 基于大吞吐量而設(shè)計(jì),每一部分緩存都連接著一個(gè)流處理器(stream processor),更加適合大規(guī)模的并行計(jì)算。
圖像渲染流程粗粒度地大概分為下面這些步驟:
上述圖像渲染流水線中,除了第一部分 Application 階段,后續(xù)主要都由 GPU 負(fù)責(zé),為了方便后文講解,先將 GPU 的渲染流程圖展示出來(lái):
上圖就是一個(gè)三角形被渲染的過(guò)程中,GPU 所負(fù)責(zé)的渲染流水線??梢钥吹胶?jiǎn)單的三角形繪制就需要大量的計(jì)算,如果再有更多更復(fù)雜的頂點(diǎn)、顏色、紋理信息(包括 3D 紋理),那么計(jì)算量是難以想象的。這也是為什么 GPU 更適合于渲染流程。
接下來(lái),具體講解渲染流水線中各個(gè)部分的具體任務(wù):
Application 應(yīng)用處理階段:得到圖元
這個(gè)階段具體指的就是圖像在應(yīng)用中被處理的階段,此時(shí)還處于 CPU 負(fù)責(zé)的時(shí)期。在這個(gè)階段應(yīng)用可能會(huì)對(duì)圖像進(jìn)行一系列的操作或者改變,最終將新的圖像信息傳給下一階段。這部分信息被叫做圖元(primitives),通常是三角形、線段、頂點(diǎn)等。
Geometry 幾何處理階段:處理圖元
進(jìn)入這個(gè)階段之后,以及之后的階段,就都主要由 GPU 負(fù)責(zé)了。此時(shí) GPU 可以拿到上一個(gè)階段傳遞下來(lái)的圖元信息,GPU 會(huì)對(duì)這部分圖元進(jìn)行處理,之后輸出新的圖元。這一系列階段包括:
頂點(diǎn)著色器(Vertex Shader):這個(gè)階段中會(huì)將圖元中的頂點(diǎn)信息進(jìn)行視角轉(zhuǎn)換、添加光照信息、增加紋理等操作。
形狀裝配(Shape Assembly):圖元中的三角形、線段、點(diǎn)分別對(duì)應(yīng)三個(gè) Vertex、兩個(gè) Vertex、一個(gè) Vertex。這個(gè)階段會(huì)將 Vertex 連接成相對(duì)應(yīng)的形狀。
幾何著色器(Geometry Shader):額外添加額外的Vertex,將原始圖元轉(zhuǎn)換成新圖元,以構(gòu)建一個(gè)不一樣的模型。簡(jiǎn)單來(lái)說(shuō)就是基于通過(guò)三角形、線段和點(diǎn)構(gòu)建更復(fù)雜的幾何圖形。
Rasterization 光柵化階段:圖元轉(zhuǎn)換為像素
光柵化的主要目的是將幾何渲染之后的圖元信息,轉(zhuǎn)換為一系列的像素,以便后續(xù)顯示在屏幕上。這個(gè)階段中會(huì)根據(jù)圖元信息,計(jì)算出每個(gè)圖元所覆蓋的像素信息等,從而將像素劃分成不同的部分。
一種簡(jiǎn)單的劃分就是根據(jù)中心點(diǎn),如果像素的中心點(diǎn)在圖元內(nèi)部,那么這個(gè)像素就屬于這個(gè)圖元。如上圖所示,深藍(lán)色的線就是圖元信息所構(gòu)建出的三角形;而通過(guò)是否覆蓋中心點(diǎn),可以遍歷出所有屬于該圖元的所有像素,即淺藍(lán)色部分。
Pixel 像素處理階段:處理像素,得到位圖
經(jīng)過(guò)上述光柵化階段,我們得到了圖元所對(duì)應(yīng)的像素,此時(shí),我們需要給這些像素填充顏色和效果。所以最后這個(gè)階段就是給像素填充正確的內(nèi)容,最終顯示在屏幕上。這些經(jīng)過(guò)處理、蘊(yùn)含大量信息的像素點(diǎn)集合,被稱(chēng)作位圖(bitmap)。也就是說(shuō),Pixel 階段最終輸出的結(jié)果就是位圖,過(guò)程具體包含:
這些點(diǎn)可以進(jìn)行不同的排列和染色以構(gòu)成圖樣。當(dāng)放大位圖時(shí),可以看見(jiàn)賴(lài)以構(gòu)成整個(gè)圖像的無(wú)數(shù)單個(gè)方塊。只要有足夠多的不同色彩的像素,就可以制作出色彩豐富的圖象,逼真地表現(xiàn)自然界的景象??s放和旋轉(zhuǎn)容易失真,同時(shí)文件容量較大。
片段著色器(Fragment Shader):也叫做 Pixel Shader,這個(gè)階段的目的是給每一個(gè)像素 Pixel 賦予正確的顏色。顏色的來(lái)源就是之前得到的頂點(diǎn)、紋理、光照等信息。由于需要處理紋理、光照等復(fù)雜信息,所以這通常是整個(gè)系統(tǒng)的性能瓶頸。
測(cè)試與混合(Tests and Blending):也叫做 Merging 階段,這個(gè)階段主要處理片段的前后位置以及透明度。這個(gè)階段會(huì)檢測(cè)各個(gè)著色片段的深度值 z 坐標(biāo),從而判斷片段的前后位置,以及是否應(yīng)該被舍棄。同時(shí)也會(huì)計(jì)算相應(yīng)的透明度 alpha 值,從而進(jìn)行片段的混合,得到最終的顏色。
在圖像渲染流程結(jié)束之后,接下來(lái)就需要將得到的像素信息顯示在物理屏幕上了。GPU 最后一步渲染結(jié)束之后像素信息,被存在幀緩沖器(Framebuffer)中,之后視頻控制器(Video Controller)會(huì)讀取幀緩沖器中的信息,經(jīng)過(guò)數(shù)模轉(zhuǎn)換傳遞給顯示器(Monitor),進(jìn)行顯示。完整的流程如下圖所示:
經(jīng)過(guò) GPU 處理之后的像素集合,也就是位圖,會(huì)被幀緩沖器緩存起來(lái),供之后的顯示使用。顯示器的電子束會(huì)從屏幕的左上角開(kāi)始逐行掃描,屏幕上的每個(gè)點(diǎn)的圖像信息都從幀緩沖器中的位圖進(jìn)行讀取,在屏幕上對(duì)應(yīng)地顯示。掃描的流程如下圖所示:
電子束掃描的過(guò)程中,屏幕就能呈現(xiàn)出對(duì)應(yīng)的結(jié)果,每次整個(gè)屏幕被掃描完一次后,就相當(dāng)于呈現(xiàn)了一幀完整的圖像。屏幕不斷地刷新,不停呈現(xiàn)新的幀,就能呈現(xiàn)出連續(xù)的影像。而這個(gè)屏幕刷新的頻率,就是幀率(Frame per Second,F(xiàn)PS)。由于人眼的視覺(jué)暫留效應(yīng),當(dāng)屏幕刷新頻率足夠高時(shí)(FPS 通常是 50 到 60 左右),就能讓畫(huà)面看起來(lái)是連續(xù)而流暢的。對(duì)于 iOS 而言,app 應(yīng)該盡量保證 60 FPS 才是最好的體驗(yàn)。
在這種單一緩存的模式下,最理想的情況就是一個(gè)流暢的流水線:每次電子束從頭開(kāi)始新的一幀的掃描時(shí),CPU+GPU 對(duì)于該幀的渲染流程已經(jīng)結(jié)束,渲染好的位圖已經(jīng)放入幀緩沖器中。但這種完美的情況是非常脆弱的,很容易產(chǎn)生屏幕撕裂:
CPU+GPU 的渲染流程是一個(gè)非常耗時(shí)的過(guò)程。如果在電子束開(kāi)始掃描新的一幀時(shí),位圖還沒(méi)有渲染好,而是在掃描到屏幕中間時(shí)才渲染完成,被放入幀緩沖器中 —— 那么已掃描的部分就是上一幀的畫(huà)面,而未掃描的部分則會(huì)顯示新的一幀圖像,這就造成屏幕撕裂。
解決屏幕撕裂、提高顯示效率的一個(gè)策略就是使用垂直同步信號(hào) Vsync 與雙緩沖機(jī)制 Double Buffering。根據(jù)蘋(píng)果的官方文檔描述,iOS 設(shè)備會(huì)始終使用 Vsync + Double Buffering 的策略。
垂直同步信號(hào)(vertical synchronisation,Vsync)相當(dāng)于給幀緩沖器加鎖:當(dāng)電子束完成一幀的掃描,將要從頭開(kāi)始掃描時(shí),就會(huì)發(fā)出一個(gè)垂直同步信號(hào)。只有當(dāng)視頻控制器接收到 Vsync 之后,才會(huì)將幀緩沖器中的位圖更新為下一幀,這樣就能保證每次顯示的都是同一幀的畫(huà)面,因而避免了屏幕撕裂。
但是這種情況下,視頻控制器在接受到 Vsync 之后,就要將下一幀的位圖傳入,這意味著整個(gè) CPU+GPU 的渲染流程都要在一瞬間完成,這是明顯不現(xiàn)實(shí)的。所以雙緩沖機(jī)制會(huì)增加一個(gè)新的備用緩沖器(back buffer)。渲染結(jié)果會(huì)預(yù)先保存在 back buffer 中,在接收到 Vsync 信號(hào)的時(shí)候,視頻控制器會(huì)將 back buffer 中的內(nèi)容置換到 frame buffer 中,此時(shí)就能保證置換操作幾乎在一瞬間完成(實(shí)際上是交換了內(nèi)存地址)。
啟用 Vsync 信號(hào)以及雙緩沖機(jī)制之后,能夠解決屏幕撕裂的問(wèn)題,但是會(huì)引入新的問(wèn)題:掉幀。如果在接收到 Vsync 之時(shí) CPU 和 GPU 還沒(méi)有渲染好新的位圖,視頻控制器就不會(huì)去替換 frame buffer 中的位圖。這時(shí)屏幕就會(huì)重新掃描呈現(xiàn)出上一幀一模一樣的畫(huà)面。相當(dāng)于兩個(gè)周期顯示了同樣的畫(huà)面,這就是所謂掉幀的情況。
如圖所示,A、B 代表兩個(gè)幀緩沖器,當(dāng) B 沒(méi)有渲染完畢時(shí)就接收到了 Vsync 信號(hào),所以屏幕只能再顯示相同幀 A,這就發(fā)生了第一次的掉幀。
事實(shí)上上述策略還有優(yōu)化空間。我們注意到在發(fā)生掉幀的時(shí)候,CPU 和 GPU 有一段時(shí)間處于閑置狀態(tài):當(dāng) A 的內(nèi)容正在被掃描顯示在屏幕上,而 B 的內(nèi)容已經(jīng)被渲染好,此時(shí) CPU 和 GPU 就處于閑置狀態(tài)。那么如果我們?cè)黾右粋€(gè)幀緩沖器,就可以利用這段時(shí)間進(jìn)行下一步的渲染,并將渲染結(jié)果暫存于新增的幀緩沖器中。
如圖所示,由于增加了新的幀緩沖器,可以一定程度上地利用掉幀的空檔期,合理利用 CPU 和 GPU 性能,從而減少掉幀的次數(shù)。
手機(jī)使用卡頓的直接原因,就是掉幀。前文也說(shuō)過(guò),屏幕刷新頻率必須要足夠高才能流暢。對(duì)于 iPhone 手機(jī)來(lái)說(shuō),屏幕最大的刷新頻率是 60 FPS,一般只要保證 50 FPS 就已經(jīng)是較好的體驗(yàn)了。但是如果掉幀過(guò)多,導(dǎo)致刷新頻率過(guò)低,就會(huì)造成不流暢的使用體驗(yàn)。
這樣看來(lái),可以大概總結(jié)一下
屏幕卡頓的根本原因:CPU 和 GPU 渲染流水線耗時(shí)過(guò)長(zhǎng),導(dǎo)致掉幀。
Vsync 與雙緩沖的意義:強(qiáng)制同步屏幕刷新,以掉幀為代價(jià)解決屏幕撕裂問(wèn)題。
三緩沖的意義:合理使用 CPU、GPU 渲染性能,減少掉幀次數(shù)。
iOS 的渲染框架依然符合渲染流水線的基本架構(gòu),具體的技術(shù)棧如上圖所示。在硬件基礎(chǔ)之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多種軟件框架來(lái)繪制內(nèi)容,在 CPU 與 GPU 之間進(jìn)行了更高層地封裝。
GPU Driver:上述軟件框架相互之間也有著依賴(lài)關(guān)系,不過(guò)所有框架最終都會(huì)通過(guò) OpenGL 連接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代碼塊,直接與 GPU 連接。
OpenGL:是一個(gè)提供了 2D 和 3D 圖形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,實(shí)現(xiàn)硬件加速渲染。OpenGL的高效實(shí)現(xiàn)(利用了圖形加速硬件)一般由顯示設(shè)備廠商提供,而且非常依賴(lài)于該廠商提供的硬件。OpenGL 之上擴(kuò)展出很多東西,如 Core Graphics 等最終都依賴(lài)于 OpenGL,有些情況下為了更高的效率,比如游戲程序,甚至?xí)苯诱{(diào)用 OpenGL 的接口。
Core Graphics:Core Graphics 是一個(gè)強(qiáng)大的二維圖像繪制引擎,是 iOS 的核心圖形庫(kù),常用的比如 CGRect 就定義在這個(gè)框架下。
Core Animation:在 iOS 上,幾乎所有的東西都是通過(guò) Core Animation 繪制出來(lái),它的自由度更高,使用范圍也更廣。
Core Image:Core Image 是一個(gè)高性能的圖像處理分析的框架,它擁有一系列現(xiàn)成的圖像濾鏡,能對(duì)已存在的圖像進(jìn)行高效的處理。
Metal:Metal 類(lèi)似于 OpenGL ES,也是一套第三方標(biāo)準(zhǔn),具體實(shí)現(xiàn)由蘋(píng)果實(shí)現(xiàn)。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構(gòu)建于 Metal 之上的。
Render, compose, and animate visual elements. —— Apple
Core Animation,它本質(zhì)上可以理解為一個(gè)復(fù)合引擎,主要職責(zé)包含:渲染、構(gòu)建和實(shí)現(xiàn)動(dòng)畫(huà)。
通常我們會(huì)使用 Core Animation 來(lái)高效、方便地實(shí)現(xiàn)動(dòng)畫(huà),但是實(shí)際上它的前身叫做 Layer Kit,關(guān)于動(dòng)畫(huà)實(shí)現(xiàn)只是它功能中的一部分。對(duì)于 iOS app,不論是否直接使用了 Core Animation,它都在底層深度參與了 app 的構(gòu)建。而對(duì)于 OS X app,也可以通過(guò)使用 Core Animation 方便地實(shí)現(xiàn)部分功能。
Core Animation 是 AppKit 和 UIKit 完美的底層支持,同時(shí)也被整合進(jìn)入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和構(gòu)建的最基礎(chǔ)架構(gòu)。Core Animation 的職責(zé)就是盡可能快地組合屏幕上不同的可視內(nèi)容,這個(gè)內(nèi)容是被分解成獨(dú)立的 layer(iOS 中具體而言就是 CALayer),并且被存儲(chǔ)為樹(shù)狀層級(jí)結(jié)構(gòu)。這個(gè)樹(shù)也形成了 UIKit 以及在 iOS 應(yīng)用程序當(dāng)中你所能在屏幕上看見(jiàn)的一切的基礎(chǔ)。
簡(jiǎn)單來(lái)說(shuō)就是用戶(hù)能看到的屏幕上的內(nèi)容都由 CALayer 進(jìn)行管理。那么 CALayer 究竟是如何進(jìn)行管理的呢?另外在 iOS 開(kāi)發(fā)過(guò)程中,最大量使用的視圖控件實(shí)際上是 UIView 而不是 CALayer,那么他們兩者的關(guān)系到底如何呢?
簡(jiǎn)單理解,CALayer 就是屏幕顯示的基礎(chǔ)。那 CALayer 是如何完成的呢?讓我們來(lái)從源碼向下探索一下,在 CALayer.h 中,CALayer 有這樣一個(gè)屬性 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)容,是一個(gè)指針類(lèi)型,在 iOS 中的類(lèi)型就是 CGImageRef(在 OS X 中還可以是 NSImage)。而我們進(jìn)一步查到,Apple 對(duì) CGImageRef 的定義是:
A bitmap image or image mask.
看到 bitmap,這下我們就可以和之前講的的渲染流水線聯(lián)系起來(lái)了:實(shí)際上,CALayer 中的 contents 屬性保存了由設(shè)備渲染流水線渲染好的位圖 bitmap(通常也被稱(chēng)為 backing store),而當(dāng)設(shè)備屏幕進(jìn)行刷新時(shí),會(huì)從 CALayer 中讀取生成好的 bitmap,進(jìn)而呈現(xiàn)到屏幕上。
所以,如果我們?cè)诖a中對(duì) CALayer 的 contents 屬性進(jìn)行了設(shè)置,比如這樣:
// 注意 CGImage 和 CGImageRef 的關(guān)系:// typedef struct CGImage CGImageRef;layer.contents = (__bridge id)image.CGImage;**
那么在運(yùn)行時(shí),操作系統(tǒng)會(huì)調(diào)用底層的接口,將 image 通過(guò) CPU+GPU 的渲染流水線渲染得到對(duì)應(yīng)的 bitmap,存儲(chǔ)于 CALayer.contents 中,在設(shè)備屏幕進(jìn)行刷新的時(shí)候就會(huì)讀取 bitmap 在屏幕上呈現(xiàn)。
也正因?yàn)槊看我讳秩镜膬?nèi)容是被靜態(tài)的存儲(chǔ)起來(lái)的,所以每次渲染時(shí),Core Animation 會(huì)觸發(fā)調(diào)用
drawRect:
方法,使用存儲(chǔ)好的 bitmap 進(jìn)行新一輪的展示。
UIView 作為最常用的視圖控件,和 CALayer 也有著千絲萬(wàn)縷的聯(lián)系,那么兩者之間到底是個(gè)什么關(guān)系,他們有什么差異?
當(dāng)然,兩者有很多顯性的區(qū)別,比如是否能夠響應(yīng)點(diǎn)擊事件。但為了從根本上徹底搞懂這些問(wèn)題,我們必須要先搞清楚兩者的職責(zé)。
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.
根據(jù) Apple 的官方文檔,UIView 是 app 中的基本組成結(jié)構(gòu),定義了一些統(tǒng)一的規(guī)范。它會(huì)負(fù)責(zé)內(nèi)容的渲染以及,處理交互事件。具體而言,它負(fù)責(zé)的事情可以歸為下面三類(lèi)
Drawing and animation:繪制與動(dòng)畫(huà)
Layout and subview management:布局與子 view 的管理
Event handling:點(diǎn)擊事件處理
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 的主要職責(zé)是管理內(nèi)部的可視內(nèi)容,這也和我們前文所講的內(nèi)容吻合。當(dāng)我們創(chuàng)建一個(gè) UIView 的時(shí)候,UIView 會(huì)自動(dòng)創(chuàng)建一個(gè) CALayer,為自身提供存儲(chǔ) bitmap 的地方(也就是前文說(shuō)的 backing store),并將自身固定設(shè)置為 CALayer 的代理。
從這兒我們大概總結(jié)出下面兩個(gè)核心關(guān)系:
CALayer 是 UIView 的屬性之一,負(fù)責(zé)渲染和動(dòng)畫(huà),提供可視內(nèi)容的呈現(xiàn)。
UIView 提供了對(duì) CALayer 部分功能的封裝,同時(shí)也另外負(fù)責(zé)了交互事件的處理。
有了這兩個(gè)最關(guān)鍵的根本關(guān)系,那么下面這些經(jīng)常出現(xiàn)在面試答案里的顯性的異同就很好解釋了。舉幾個(gè)例子:
相同的層級(jí)結(jié)構(gòu):我們對(duì) UIView 的層級(jí)結(jié)構(gòu)非常熟悉,由于每個(gè) UIView 都對(duì)應(yīng) CALayer 負(fù)責(zé)頁(yè)面的繪制,所以 CALayer 也具有相應(yīng)的層級(jí)結(jié)構(gòu)。
部分效果的設(shè)置:因?yàn)?UIView 只對(duì) CALayer 的部分功能進(jìn)行了封裝,而另一部分如圓角、陰影、邊框等特效都需要通過(guò)調(diào)用 layer 屬性來(lái)設(shè)置。
是否響應(yīng)點(diǎn)擊事件:CALayer 不負(fù)責(zé)點(diǎn)擊事件,所以不響應(yīng)點(diǎn)擊事件,而 UIView 會(huì)響應(yīng)。
不同繼承關(guān)系:CALayer 繼承自 NSObject,UIView 由于要負(fù)責(zé)交互事件,所以繼承自 UIResponder。
當(dāng)然還剩最后一個(gè)問(wèn)題,為什么要將 CALayer 獨(dú)立出來(lái),直接使用 UIView 統(tǒng)一管理不行嗎?為什么不用一個(gè)統(tǒng)一的對(duì)象來(lái)處理所有事情呢?
這樣設(shè)計(jì)的主要原因就是為了職責(zé)分離,拆分功能,方便代碼的復(fù)用。通過(guò) Core Animation 框架來(lái)負(fù)責(zé)可視內(nèi)容的呈現(xiàn),這樣在 iOS 和 OS X 上都可以使用 Core Animation 進(jìn)行渲染。與此同時(shí),兩個(gè)系統(tǒng)還可以根據(jù)交互規(guī)則的不同來(lái)進(jìn)一步封裝統(tǒng)一的控件,比如 iOS 有 UIKit 和 UIView,OS X 則是AppKit 和 NSView。
當(dāng)我們了解了 Core Animation 以及 CALayer 的基本知識(shí)后,接下來(lái)我們來(lái)看下 Core Animation 的渲染流水線。
整個(gè)流水線一共有下面幾個(gè)步驟:
Handle Events:這個(gè)過(guò)程中會(huì)先處理點(diǎn)擊事件,這個(gè)過(guò)程中有可能會(huì)需要改變頁(yè)面的布局和界面層次。
Commit Transaction:此時(shí) app 會(huì)通過(guò) CPU 處理顯示內(nèi)容的前置計(jì)算,比如布局計(jì)算、圖片解碼等任務(wù),接下來(lái)會(huì)進(jìn)行詳細(xì)的講解。之后將計(jì)算好的圖層進(jìn)行打包發(fā)給
Render Server
。
Decode:打包好的圖層被傳輸?shù)?
Render Server
之后,首先會(huì)進(jìn)行解碼。注意完成解碼之后需要等待下一個(gè) RunLoop 才會(huì)執(zhí)行下一步
Draw Calls
。
Draw Calls:解碼完成后,Core Animation 會(huì)調(diào)用下層渲染框架(比如 OpenGL 或者 Metal)的方法進(jìn)行繪制,進(jìn)而調(diào)用到 GPU。
Render:這一階段主要由 GPU 進(jìn)行渲染。
Display:顯示階段,需要等
render
結(jié)束的下一個(gè) RunLoop 觸發(fā)顯示。
一般開(kāi)發(fā)當(dāng)中能影響到的就是 Handle Events 和 Commit Transaction 這兩個(gè)階段,這也是開(kāi)發(fā)者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進(jìn)行的是:Layout、Display、Prepare、Commit 等四個(gè)具體的操作。
Layout:構(gòu)建視圖
這個(gè)階段主要處理視圖的構(gòu)建和布局,具體步驟包括:
調(diào)用重載的
layoutSubviews
方法
創(chuàng)建視圖,并通過(guò)
addSubview
方法添加子視圖
計(jì)算視圖布局,即所有的 Layout Constraint
由于這個(gè)階段是在 CPU 中進(jìn)行,通常是 CPU 限制或者 IO 限制,所以我們應(yīng)該盡量高效輕量地操作,減少這部分的時(shí)間,比如減少非必要的視圖創(chuàng)建、簡(jiǎn)化布局計(jì)算、減少視圖層級(jí)等。
Display:繪制視圖
這個(gè)階段主要是交給 Core Graphics 進(jìn)行視圖的繪制,注意不是真正的顯示,而是得到前文所說(shuō)的圖元 primitives 數(shù)據(jù):
根據(jù)上一階段 Layout 的結(jié)果創(chuàng)建得到圖元信息。
如果重寫(xiě)了
drawRect:
方法,那么會(huì)調(diào)用重載的
drawRect:
方法,在
drawRect:
方法中手動(dòng)繪制得到 bitmap 數(shù)據(jù),從而自定義視圖的繪制。
注意正常情況下 Display 階段只會(huì)得到圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據(jù)圖元信息繪制得到的。但是如果重寫(xiě)了
drawRect:
方法,這個(gè)方法會(huì)直接調(diào)用 Core Graphics 繪制方法得到 bitmap 數(shù)據(jù),同時(shí)系統(tǒng)會(huì)額外申請(qǐng)一塊內(nèi)存,用于暫存繪制好的 bitmap。
由于重寫(xiě)了 drawRect:
方法,導(dǎo)致繪制過(guò)程從 GPU 轉(zhuǎn)移到了 CPU,這就導(dǎo)致了一定的效率損失。與此同時(shí),這個(gè)過(guò)程會(huì)額外使用 CPU 和內(nèi)存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內(nèi)存爆炸。
Prepare:Core Animation 額外的工作
這一步主要是:圖片解碼和轉(zhuǎn)換
Commit:打包并發(fā)送
這一步主要是:圖層打包并發(fā)送到 Render Server。
注意 commit 操作是依賴(lài)圖層樹(shù)遞歸執(zhí)行的,所以如果圖層樹(shù)過(guò)于復(fù)雜,commit 的開(kāi)銷(xiāo)就會(huì)很大。這也是我們希望減少視圖層級(jí),從而降低圖層樹(shù)復(fù)雜度的原因。
Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 為例,那么上圖主要是 GPU 中執(zhí)行的操作,具體主要包括:
GPU 收到 Command Buffer,包含圖元 primitives 信息
Tiler 開(kāi)始工作:先通過(guò)頂點(diǎn)著色器 Vertex Shader 對(duì)頂點(diǎn)進(jìn)行處理,更新圖元信息
平鋪過(guò)程:平鋪生成 tile bucket 的幾何圖形,這一步會(huì)將圖元信息轉(zhuǎn)化為像素,之后將結(jié)果寫(xiě)入 Parameter Buffer 中
Tiler 更新完所有的圖元信息,或者 Parameter Buffer 已滿(mǎn),則會(huì)開(kāi)始下一步
Renderer 工作:將像素信息進(jìn)行處理得到 bitmap,之后存入 Render Buffer
Render Buffer 中存儲(chǔ)有渲染好的 bitmap,供之后的 Display 操作使用
使用 Instrument 的 OpenGL ES,可以對(duì)過(guò)程進(jìn)行監(jiān)控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分別監(jiān)控 Tiler 和 Renderer 的工作情況
離屏渲染作為一個(gè)面試高頻問(wèn)題,時(shí)常被提及,下面來(lái)從頭到尾講一下離屏渲染。
根據(jù)前文,簡(jiǎn)化來(lái)看,通常的渲染流程是這樣的:
App 通過(guò) CPU 和 GPU 的合作,不停地將內(nèi)容渲染完成放入 Framebuffer 幀緩沖器中,而顯示屏幕不斷地從 Framebuffer 中獲取內(nèi)容,顯示實(shí)時(shí)的內(nèi)容。
而離屏渲染的流程是這樣的:
與普通情況下 GPU 直接將渲染好的內(nèi)容放入 Framebuffer 中不同,需要先額外創(chuàng)建離屏渲染緩沖區(qū) Offscreen Buffer,將提前渲染好的內(nèi)容放入其中,等到合適的時(shí)機(jī)再將 Offscreen Buffer 中的內(nèi)容進(jìn)一步疊加、渲染,完成后將結(jié)果切換到 Framebuffer 中。
從上面的流程來(lái)看,離屏渲染時(shí)由于 App 需要提前對(duì)部分內(nèi)容進(jìn)行額外的渲染并保存到 Offscreen Buffer,以及需要在必要時(shí)刻對(duì) Offscreen Buffer 和 Framebuffer 進(jìn)行內(nèi)容切換,所以會(huì)需要更長(zhǎng)的處理時(shí)間(實(shí)際上這兩步關(guān)于 buffer 的切換代價(jià)都非常大)。
并且 Offscreen Buffer 本身就需要額外的空間,大量的離屏渲染可能早能內(nèi)存的過(guò)大壓力。與此同時(shí),Offscreen Buffer 的總大小也有限,不能超過(guò)屏幕總像素的 2.5 倍。
可見(jiàn)離屏渲染的開(kāi)銷(xiāo)非常大,一旦需要離屏渲染的內(nèi)容過(guò)多,很容易造成掉幀的問(wèn)題。所以大部分情況下,我們都應(yīng)該盡量避免離屏渲染。
那么為什么要使用離屏渲染呢?主要是因?yàn)橄旅孢@兩種原因:
一些特殊效果需要使用額外的 Offscreen Buffer 來(lái)保存渲染的中間狀態(tài),所以不得不使用離屏渲染。
處于效率目的,可以將內(nèi)容提前渲染保存在 Offscreen Buffer 中,達(dá)到復(fù)用的目的。
對(duì)于第一種情況,也就是不得不使用離屏渲染的情況,一般都是系統(tǒng)自動(dòng)觸發(fā)的,比如陰影、圓角等等。
最常見(jiàn)的情形之一就是:使用了 mask 蒙版。
如圖所示,由于最終的內(nèi)容是由兩層渲染結(jié)果疊加,所以必須要利用額外的內(nèi)存空間對(duì)中間的渲染結(jié)果進(jìn)行保存,因此系統(tǒng)會(huì)默認(rèn)觸發(fā)離屏渲染。
又比如下面這個(gè)例子,iOS 8 開(kāi)始提供的模糊特效 UIBlurEffectView:
整個(gè)模糊過(guò)程分為多步:Pass 1 先渲染需要模糊的內(nèi)容本身,Pass 2 對(duì)內(nèi)容進(jìn)行縮放,Pass 3 4 分別對(duì)上一步內(nèi)容進(jìn)行橫縱方向的模糊操作,最后一步用模糊后的結(jié)果疊加合成,最終實(shí)現(xiàn)完整的模糊特效。
而第二種情況,為了復(fù)用提高效率而使用離屏渲染一般是主動(dòng)的行為,是通過(guò) CALayer 的 shouldRasterize 光柵化操作實(shí)現(xiàn)的。
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.
開(kāi)啟光柵化后,會(huì)觸發(fā)離屏渲染,Render Server 會(huì)強(qiáng)制將 CALayer 的渲染位圖結(jié)果 bitmap 保存下來(lái),這樣下次再需要渲染時(shí)就可以直接復(fù)用,從而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圓角、陰影、組透明度 group opacity 等,所以如果 layer 的構(gòu)成包含上述幾種元素,結(jié)構(gòu)復(fù)雜且需要反復(fù)利用,那么就可以考慮打開(kāi)光柵化。
圓角、陰影、組透明度等會(huì)由系統(tǒng)自動(dòng)觸發(fā)離屏渲染,那么打開(kāi)光柵化可以節(jié)約第二次及以后的渲染時(shí)間。而多層 subLayer 的情況由于不會(huì)自動(dòng)觸發(fā)離屏渲染,所以相比之下會(huì)多花費(fèi)第一次離屏渲染的時(shí)間,但是可以節(jié)約后續(xù)的重復(fù)渲染的開(kāi)銷(xiāo)。
不過(guò)使用光柵化的時(shí)候需要注意以下幾點(diǎn):
如果 layer 不能被復(fù)用,則沒(méi)有必要打開(kāi)光柵化
如果 layer 不是靜態(tài),需要被頻繁修改,比如處于動(dòng)畫(huà)之中,那么開(kāi)啟離屏渲染反而影響效率
離屏渲染緩存內(nèi)容有時(shí)間限制,緩存內(nèi)容 100ms 內(nèi)如果沒(méi)有被使用,那么就會(huì)被丟棄,無(wú)法進(jìn)行復(fù)用
離屏渲染緩存空間有限,超過(guò) 2.5 倍屏幕像素大小的話(huà)也會(huì)失效,無(wú)法復(fù)用
通常來(lái)講,設(shè)置了 layer 的圓角效果之后,會(huì)自動(dòng)觸發(fā)離屏渲染。但是究竟什么情況下設(shè)置圓角才會(huì)觸發(fā)離屏渲染呢?
如上圖所示,layer 由三層組成,我們?cè)O(shè)置圓角通常會(huì)首先像下面這行代碼一樣進(jìn)行設(shè)置:
view.layer.cornerRadius = 2
根據(jù) cornerRadius - Apple 的描述,上述代碼只會(huì)默認(rèn)設(shè)置 backgroundColor 和 border 的圓角,而不會(huì)設(shè)置 content 的圓角,除非同時(shí)設(shè)置了 layer.masksToBounds 為 true(對(duì)應(yīng) 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.
如果只是設(shè)置了 cornerRadius 而沒(méi)有設(shè)置 masksToBounds,由于不需要疊加裁剪,此時(shí)是并不會(huì)觸發(fā)離屏渲染的。而當(dāng)設(shè)置了裁剪屬性的時(shí)候,由于 masksToBounds 會(huì)對(duì) layer 以及所有 subLayer 的 content 都進(jìn)行裁剪,所以不得不觸發(fā)離屏渲染。
view.layer.masksToBounds = true // 觸發(fā)離屏渲染的原因
所以,Texture 也提出在沒(méi)有必要使用圓角裁剪的時(shí)候,盡量不去觸發(fā)離屏渲染而影響效率:
剛才說(shuō)了圓角加上 masksToBounds 的時(shí)候,因?yàn)?masksToBounds 會(huì)對(duì) layer 上的所有內(nèi)容進(jìn)行裁剪,從而誘發(fā)了離屏渲染,那么這個(gè)過(guò)程具體是怎么回事呢,下面我們來(lái)仔細(xì)講一下。
圖層的疊加繪制大概遵循“畫(huà)家算法”,在這種算法下會(huì)按層繪制,首先繪制距離較遠(yuǎn)的場(chǎng)景,然后用繪制距離較近的場(chǎng)景覆蓋較遠(yuǎn)的部分。
在普通的 layer 繪制中,上層的 sublayer 會(huì)覆蓋下層的 sublayer,下層 sublayer 繪制完之后就可以?huà)仐壛?,從而?jié)約空間提高效率。所有 sublayer 依次繪制完畢之后,整個(gè)繪制過(guò)程完成,就可以進(jìn)行后續(xù)的呈現(xiàn)了。假設(shè)我們需要繪制一個(gè)三層的 sublayer,不設(shè)置裁剪和圓角,那么整個(gè)繪制過(guò)程就如下圖所示:
而當(dāng)我們?cè)O(shè)置了 cornerRadius 以及 masksToBounds 進(jìn)行圓角 + 裁剪時(shí),如前文所述,masksToBounds 裁剪屬性會(huì)應(yīng)用到所有的 sublayer 上。這也就意味著所有的 sublayer 必須要重新被應(yīng)用一次圓角+裁剪,這也就意味著所有的 sublayer 在第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer 中等待下一輪圓角+裁剪,這也就誘發(fā)了離屏渲染,具體過(guò)程如下:
實(shí)際上不只是圓角+裁剪,如果設(shè)置了透明度+組透明(layer.allowsGroupOpacity
+layer.opacity
),陰影屬性(shadowOffset
等)都會(huì)產(chǎn)生類(lèi)似的效果,因?yàn)榻M透明度、陰影都是和裁剪類(lèi)似的,會(huì)作用與 layer 以及其所有 sublayer 上,這就導(dǎo)致必然會(huì)引起離屏渲染。
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎?
由于剛才我們提到,圓角引起離屏渲染的本質(zhì)是裁剪的疊加,導(dǎo)致 masksToBounds 對(duì) layer 以及所有 sublayer 進(jìn)行二次處理。那么我們只要避免使用 masksToBounds 進(jìn)行二次處理,而是對(duì)所有的 sublayer 進(jìn)行預(yù)處理,就可以只進(jìn)行“畫(huà)家算法”,用一次疊加就完成繪制。
那么可行的實(shí)現(xiàn)方法大概有下面幾種:
【換資源】直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過(guò)這種方法需要依賴(lài)具體情況,并不通用。
【mask】再增加一個(gè)和背景色相同的遮罩 mask 覆蓋在最上層,蓋住四個(gè)角,營(yíng)造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。
【UIBezierPath】用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設(shè)置只有內(nèi)部可見(jiàn),再將不帶圓角的 layer 渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是 layer 的布局一旦改變,貝塞爾曲線都需要手動(dòng)地重新繪制,所以需要對(duì) frame、color 等進(jìn)行手動(dòng)地監(jiān)聽(tīng)并重繪。
【CoreGraphics】重寫(xiě)
drawRect:
,用 CoreGraphics 相關(guān)方法,在需要應(yīng)用圓角時(shí)進(jìn)行手動(dòng)繪制。不過(guò) CoreGraphics 效率也很有限,如果需要多次調(diào)用也會(huì)有效率問(wèn)題。
感謝各位的閱讀,以上就是“iOS渲染原理是什么”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)iOS渲染原理是什么這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。