Python實(shí)戰(zhàn)社群 Java實(shí)戰(zhàn)社群 長(zhǎng)按識(shí)別下方二維碼,按需求添加 掃碼關(guān)注添加客服 進(jìn)Python社群▲ 掃碼關(guān)注添加客服 進(jìn)Java社群▲ 作者丨Rickey 來源丨一瓜技術(shù)(tech_gua) 0. 本文知識(shí)目錄1. 計(jì)算機(jī)渲染原理CPU 與 GPU 的架構(gòu)對(duì)于現(xiàn)代計(jì)算機(jī)系統(tǒng),簡(jiǎn)單來說可以大概視作三層架構(gòu):硬件、操作系統(tǒng)與進(jìn)程。對(duì)于移動(dòng)端來說,進(jìn)程就是 app,而 CPU 與 GPU 是硬件層面的重要組成部分。CPU 與 GPU 提供了計(jì)算能力,通過操作系統(tǒng)被 app 調(diào)用。
CPU 和 GPU 其設(shè)計(jì)目標(biāo)就是不同的,它們分別針對(duì)了兩種不同的應(yīng)用場(chǎng)景。CPU 是運(yùn)算核心與控制核心,需要有很強(qiáng)的運(yùn)算通用性,兼容各種數(shù)據(jù)類型,同時(shí)也需要能處理大量不同的跳轉(zhuǎn)、中斷等指令,因此 CPU 的內(nèi)部結(jié)構(gòu)更為復(fù)雜。而 GPU 則面對(duì)的是類型統(tǒng)一、更加單純的運(yùn)算,也不需要處理復(fù)雜的指令,但也肩負(fù)著更大的運(yùn)算任務(wù)。 而 GPU 擁有更多的計(jì)算單元 Arithmetic Logic Unit,具有更強(qiáng)的計(jì)算能力,同時(shí)也具有更多的控制單元。GPU 基于大吞吐量而設(shè)計(jì),每一部分緩存都連接著一個(gè)流處理器(stream processor),更加適合大規(guī)模的并行計(jì)算。 圖像渲染流水線圖像渲染流程粗粒度地大概分為下面這些步驟: 上述圖像渲染流水線中,除了第一部分 Application 階段,后續(xù)主要都由 GPU 負(fù)責(zé),為了方便后文講解,先將 GPU 的渲染流程圖展示出來: 上圖就是一個(gè)三角形被渲染的過程中,GPU 所負(fù)責(zé)的渲染流水線??梢钥吹胶?jiǎn)單的三角形繪制就需要大量的計(jì)算,如果再有更多更復(fù)雜的頂點(diǎn)、顏色、紋理信息(包括 3D 紋理),那么計(jì)算量是難以想象的。這也是為什么 GPU 更適合于渲染流程。 接下來,具體講解渲染流水線中各個(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è)階段傳遞下來的圖元信息,GPU 會(huì)對(duì)這部分圖元進(jìn)行處理,之后輸出新的圖元。這一系列階段包括:
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)建出的三角形;而通過是否覆蓋中心點(diǎn),可以遍歷出所有屬于該圖元的所有像素,即淺藍(lán)色部分。 Pixel 像素處理階段:處理像素,得到位圖 經(jīng)過上述光柵化階段,我們得到了圖元所對(duì)應(yīng)的像素,此時(shí),我們需要給這些像素填充顏色和效果。所以最后這個(gè)階段就是給像素填充正確的內(nèi)容,最終顯示在屏幕上。這些經(jīng)過處理、蘊(yùn)含大量信息的像素點(diǎn)集合,被稱作位圖(bitmap)。也就是說,Pixel 階段最終輸出的結(jié)果就是位圖,過程具體包含: 這些點(diǎn)可以進(jìn)行不同的排列和染色以構(gòu)成圖樣。當(dāng)放大位圖時(shí),可以看見賴以構(gòu)成整個(gè)圖像的無數(shù)單個(gè)方塊。只要有足夠多的不同色彩的像素,就可以制作出色彩豐富的圖象,逼真地表現(xiàn)自然界的景象??s放和旋轉(zhuǎn)容易失真,同時(shí)文件容量較大。
2. 屏幕成像與卡頓在圖像渲染流程結(jié)束之后,接下來就需要將得到的像素信息顯示在物理屏幕上了。GPU 最后一步渲染結(jié)束之后像素信息,被存在幀緩沖器(Framebuffer)中,之后視頻控制器(Video Controller)會(huì)讀取幀緩沖器中的信息,經(jīng)過數(shù)模轉(zhuǎn)換傳遞給顯示器(Monitor),進(jìn)行顯示。完整的流程如下圖所示: 經(jīng)過 GPU 處理之后的像素集合,也就是位圖,會(huì)被幀緩沖器緩存起來,供之后的顯示使用。顯示器的電子束會(huì)從屏幕的左上角開始逐行掃描,屏幕上的每個(gè)點(diǎn)的圖像信息都從幀緩沖器中的位圖進(jìn)行讀取,在屏幕上對(duì)應(yīng)地顯示。掃描的流程如下圖所示: 電子束掃描的過程中,屏幕就能呈現(xiàn)出對(duì)應(yīng)的結(jié)果,每次整個(gè)屏幕被掃描完一次后,就相當(dāng)于呈現(xiàn)了一幀完整的圖像。屏幕不斷地刷新,不停呈現(xiàn)新的幀,就能呈現(xiàn)出連續(xù)的影像。而這個(gè)屏幕刷新的頻率,就是幀率(Frame per Second,F(xiàn)PS)。由于人眼的視覺暫留效應(yīng),當(dāng)屏幕刷新頻率足夠高時(shí)(FPS 通常是 50 到 60 左右),就能讓畫面看起來是連續(xù)而流暢的。對(duì)于 iOS 而言,app 應(yīng)該盡量保證 60 FPS 才是最好的體驗(yàn)。 屏幕撕裂 Screen Tearing在這種單一緩存的模式下,最理想的情況就是一個(gè)流暢的流水線:每次電子束從頭開始新的一幀的掃描時(shí),CPU+GPU 對(duì)于該幀的渲染流程已經(jīng)結(jié)束,渲染好的位圖已經(jīng)放入幀緩沖器中。但這種完美的情況是非常脆弱的,很容易產(chǎn)生屏幕撕裂: CPU+GPU 的渲染流程是一個(gè)非常耗時(shí)的過程。如果在電子束開始掃描新的一幀時(shí),位圖還沒有渲染好,而是在掃描到屏幕中間時(shí)才渲染完成,被放入幀緩沖器中 ---- 那么已掃描的部分就是上一幀的畫面,而未掃描的部分則會(huì)顯示新的一幀圖像,這就造成屏幕撕裂。 垂直同步 Vsync + 雙緩沖機(jī)制 Double Buffering解決屏幕撕裂、提高顯示效率的一個(gè)策略就是使用垂直同步信號(hào) Vsync 與雙緩沖機(jī)制 Double Buffering。根據(jù)蘋果的官方文檔描述,iOS 設(shè)備會(huì)始終使用 Vsync + Double Buffering 的策略。 垂直同步信號(hào)(vertical synchronisation,Vsync)相當(dāng)于給幀緩沖器加鎖:當(dāng)電子束完成一幀的掃描,將要從頭開始掃描時(shí),就會(huì)發(fā)出一個(gè)垂直同步信號(hào)。只有當(dāng)視頻控制器接收到 Vsync 之后,才會(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)存地址)。 掉幀 Jank啟用 Vsync 信號(hào)以及雙緩沖機(jī)制之后,能夠解決屏幕撕裂的問題,但是會(huì)引入新的問題:掉幀。如果在接收到 Vsync 之時(shí) CPU 和 GPU 還沒有渲染好新的位圖,視頻控制器就不會(huì)去替換 frame buffer 中的位圖。這時(shí)屏幕就會(huì)重新掃描呈現(xiàn)出上一幀一模一樣的畫面。相當(dāng)于兩個(gè)周期顯示了同樣的畫面,這就是所謂掉幀的情況。 如圖所示,A、B 代表兩個(gè)幀緩沖器,當(dāng) B 沒有渲染完畢時(shí)就接收到了 Vsync 信號(hào),所以屏幕只能再顯示相同幀 A,這就發(fā)生了第一次的掉幀。 三緩沖 Triple Buffering事實(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ù)。 屏幕卡頓的本質(zhì)手機(jī)使用卡頓的直接原因,就是掉幀。前文也說過,屏幕刷新頻率必須要足夠高才能流暢。對(duì)于 iPhone 手機(jī)來說,屏幕最大的刷新頻率是 60 FPS,一般只要保證 50 FPS 就已經(jīng)是較好的體驗(yàn)了。但是如果掉幀過多,導(dǎo)致刷新頻率過低,就會(huì)造成不流暢的使用體驗(yàn)。 這樣看來,可以大概總結(jié)一下
3. iOS 中的渲染框架iOS 的渲染框架依然符合渲染流水線的基本架構(gòu),具體的技術(shù)棧如上圖所示。在硬件基礎(chǔ)之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多種軟件框架來繪制內(nèi)容,在 CPU 與 GPU 之間進(jìn)行了更高層地封裝。 GPU Driver:上述軟件框架相互之間也有著依賴關(guān)系,不過所有框架最終都會(huì)通過 OpenGL 連接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代碼塊,直接與 GPU 連接。 OpenGL:是一個(gè)提供了 2D 和 3D 圖形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,實(shí)現(xiàn)硬件加速渲染。OpenGL的高效實(shí)現(xiàn)(利用了圖形加速硬件)一般由顯示設(shè)備廠商提供,而且非常依賴于該廠商提供的硬件。OpenGL 之上擴(kuò)展出很多東西,如 Core Graphics 等最終都依賴于 OpenGL,有些情況下為了更高的效率,比如游戲程序,甚至?xí)苯诱{(diào)用 OpenGL 的接口。 Core Graphics:Core Graphics 是一個(gè)強(qiáng)大的二維圖像繪制引擎,是 iOS 的核心圖形庫,常用的比如 CGRect 就定義在這個(gè)框架下。 Core Animation:在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,它的自由度更高,使用范圍也更廣。 Core Image:Core Image 是一個(gè)高性能的圖像處理分析的框架,它擁有一系列現(xiàn)成的圖像濾鏡,能對(duì)已存在的圖像進(jìn)行高效的處理。 Metal:Metal 類似于 OpenGL ES,也是一套第三方標(biāo)準(zhǔn),具體實(shí)現(xiàn)由蘋果實(shí)現(xiàn)。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是構(gòu)建于 Metal 之上的。 Core Animation 是什么
Core Animation,它本質(zhì)上可以理解為一個(gè)復(fù)合引擎,主要職責(zé)包含:渲染、構(gòu)建和實(shí)現(xiàn)動(dòng)畫。 通常我們會(huì)使用 Core Animation 來高效、方便地實(shí)現(xiàn)動(dòng)畫,但是實(shí)際上它的前身叫做 Layer Kit,關(guān)于動(dòng)畫實(shí)現(xiàn)只是它功能中的一部分。對(duì)于 iOS app,不論是否直接使用了 Core Animation,它都在底層深度參與了 app 的構(gòu)建。而對(duì)于 OS X app,也可以通過使用 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ǔ)為樹狀層級(jí)結(jié)構(gòu)。這個(gè)樹也形成了 UIKit 以及在 iOS 應(yīng)用程序當(dāng)中你所能在屏幕上看見的一切的基礎(chǔ)。 簡(jiǎn)單來說就是用戶能看到的屏幕上的內(nèi)容都由 CALayer 進(jìn)行管理。那么 CALayer 究竟是如何進(jìn)行管理的呢?另外在 iOS 開發(fā)過程中,最大量使用的視圖控件實(shí)際上是 UIView 而不是 CALayer,那么他們兩者的關(guān)系到底如何呢? CALayer 是顯示的基礎(chǔ):存儲(chǔ) bitmap簡(jiǎn)單理解,CALayer 就是屏幕顯示的基礎(chǔ)。那 CALayer 是如何完成的呢?讓我們來從源碼向下探索一下,在 CALayer.h 中,CALayer 有這樣一個(gè)屬性 contents:
contents 提供了 layer 的內(nèi)容,是一個(gè)指針類型,在 iOS 中的類型就是 CGImageRef(在 OS X 中還可以是 NSImage)。而我們進(jìn)一步查到,Apple 對(duì) CGImageRef 的定義是:
看到 bitmap,這下我們就可以和之前講的的渲染流水線聯(lián)系起來了:實(shí)際上,CALayer 中的 contents 屬性保存了由設(shè)備渲染流水線渲染好的位圖 bitmap(通常也被稱為 backing store),而當(dāng)設(shè)備屏幕進(jìn)行刷新時(shí),會(huì)從 CALayer 中讀取生成好的 bitmap,進(jìn)而呈現(xiàn)到屏幕上。 所以,如果我們?cè)诖a中對(duì) CALayer 的 contents 屬性進(jìn)行了設(shè)置,比如這樣:
那么在運(yùn)行時(shí),操作系統(tǒng)會(huì)調(diào)用底層的接口,將 image 通過 CPU+GPU 的渲染流水線渲染得到對(duì)應(yīng)的 bitmap,存儲(chǔ)于 CALayer.contents 中,在設(shè)備屏幕進(jìn)行刷新的時(shí)候就會(huì)讀取 bitmap 在屏幕上呈現(xiàn)。 也正因?yàn)槊看我讳秩镜膬?nèi)容是被靜態(tài)的存儲(chǔ)起來的,所以每次渲染時(shí),Core Animation 會(huì)觸發(fā)調(diào)用 CALayer 與 UIView 的關(guān)系UIView 作為最常用的視圖控件,和 CALayer 也有著千絲萬縷的聯(lián)系,那么兩者之間到底是個(gè)什么關(guān)系,他們有什么差異? 當(dāng)然,兩者有很多顯性的區(qū)別,比如是否能夠響應(yīng)點(diǎn)擊事件。但為了從根本上徹底搞懂這些問題,我們必須要先搞清楚兩者的職責(zé)。
根據(jù) Apple 的官方文檔,UIView 是 app 中的基本組成結(jié)構(gòu),定義了一些統(tǒng)一的規(guī)范。它會(huì)負(fù)責(zé)內(nèi)容的渲染以及,處理交互事件。具體而言,它負(fù)責(zé)的事情可以歸為下面三類
而從 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 的地方(也就是前文說的 backing store),并將自身固定設(shè)置為 CALayer 的代理。 從這兒我們大概總結(jié)出下面兩個(gè)核心關(guān)系:
有了這兩個(gè)最關(guān)鍵的根本關(guān)系,那么下面這些經(jīng)常出現(xiàn)在面試答案里的顯性的異同就很好解釋了。舉幾個(gè)例子:
當(dāng)然還剩最后一個(gè)問題,為什么要將 CALayer 獨(dú)立出來,直接使用 UIView 統(tǒng)一管理不行嗎?為什么不用一個(gè)統(tǒng)一的對(duì)象來處理所有事情呢? 這樣設(shè)計(jì)的主要原因就是為了職責(zé)分離,拆分功能,方便代碼的復(fù)用。通過 Core Animation 框架來負(fù)責(zé)可視內(nèi)容的呈現(xiàn),這樣在 iOS 和 OS X 上都可以使用 Core Animation 進(jìn)行渲染。與此同時(shí),兩個(gè)系統(tǒng)還可以根據(jù)交互規(guī)則的不同來進(jìn)一步封裝統(tǒng)一的控件,比如 iOS 有 UIKit 和 UIView,OS X 則是AppKit 和 NSView。 4. Core Animation 渲染全內(nèi)容Core Animation Pipeline 渲染流水線當(dāng)我們了解了 Core Animation 以及 CALayer 的基本知識(shí)后,接下來我們來看下 Core Animation 的渲染流水線。 整個(gè)流水線一共有下面幾個(gè)步驟: Handle Events:這個(gè)過程中會(huì)先處理點(diǎn)擊事件,這個(gè)過程中有可能會(huì)需要改變頁面的布局和界面層次。 Commit Transaction:此時(shí) app 會(huì)通過 CPU 處理顯示內(nèi)容的前置計(jì)算,比如布局計(jì)算、圖片解碼等任務(wù),接下來會(huì)進(jìn)行詳細(xì)的講解。之后將計(jì)算好的圖層進(jìn)行打包發(fā)給 Decode:打包好的圖層被傳輸?shù)?nbsp; Draw Calls:解碼完成后,Core Animation 會(huì)調(diào)用下層渲染框架(比如 OpenGL 或者 Metal)的方法進(jìn)行繪制,進(jìn)而調(diào)用到 GPU。 Render:這一階段主要由 GPU 進(jìn)行渲染。 Display:顯示階段,需要等 Commit Transaction 發(fā)生了什么一般開發(fā)當(dāng)中能影響到的就是 Handle Events 和 Commit Transaction 這兩個(gè)階段,這也是開發(fā)者接觸最多的部分。Handle Events 就是處理觸摸事件,而 Commit Transaction 這部分中主要進(jìn)行的是:Layout、Display、Prepare、Commit 等四個(gè)具體的操作。 Layout:構(gòu)建視圖 這個(gè)階段主要處理視圖的構(gòu)建和布局,具體步驟包括:
由于這個(gè)階段是在 CPU 中進(jìn)行,通常是 CPU 限制或者 IO 限制,所以我們應(yīng)該盡量高效輕量地操作,減少這部分的時(shí)間,比如減少非必要的視圖創(chuàng)建、簡(jiǎn)化布局計(jì)算、減少視圖層級(jí)等。 Display:繪制視圖 這個(gè)階段主要是交給 Core Graphics 進(jìn)行視圖的繪制,注意不是真正的顯示,而是得到前文所說的圖元 primitives 數(shù)據(jù):
注意正常情況下 Display 階段只會(huì)得到圖元 primitives 信息,而位圖 bitmap 是在 GPU 中根據(jù)圖元信息繪制得到的。但是如果重寫了 由于重寫了 Prepare:Core Animation 額外的工作 這一步主要是:圖片解碼和轉(zhuǎn)換 Commit:打包并發(fā)送 這一步主要是:圖層打包并發(fā)送到 Render Server。 注意 commit 操作是依賴圖層樹遞歸執(zhí)行的,所以如果圖層樹過于復(fù)雜,commit 的開銷就會(huì)很大。這也是我們希望減少視圖層級(jí),從而降低圖層樹復(fù)雜度的原因。 Rendering Pass:Render Server 的具體操作Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 為例,那么上圖主要是 GPU 中執(zhí)行的操作,具體主要包括:
5. Offscreen Rendering 離屏渲染離屏渲染作為一個(gè)面試高頻問題,時(shí)常被提及,下面來從頭到尾講一下離屏渲染。 離屏渲染具體過程根據(jù)前文,簡(jiǎn)化來看,通常的渲染流程是這樣的: App 通過 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 中。 離屏渲染的效率問題從上面的流程來看,離屏渲染時(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)存的過大壓力。與此同時(shí),Offscreen Buffer 的總大小也有限,不能超過屏幕總像素的 2.5 倍。 可見離屏渲染的開銷非常大,一旦需要離屏渲染的內(nèi)容過多,很容易造成掉幀的問題。所以大部分情況下,我們都應(yīng)該盡量避免離屏渲染。 為什么使用離屏渲染那么為什么要使用離屏渲染呢?主要是因?yàn)橄旅孢@兩種原因:
對(duì)于第一種情況,也就是不得不使用離屏渲染的情況,一般都是系統(tǒng)自動(dòng)觸發(fā)的,比如陰影、圓角等等。 最常見的情形之一就是:使用了 mask 蒙版。 如圖所示,由于最終的內(nèi)容是由兩層渲染結(jié)果疊加,所以必須要利用額外的內(nèi)存空間對(duì)中間的渲染結(jié)果進(jìn)行保存,因此系統(tǒng)會(huì)默認(rèn)觸發(fā)離屏渲染。 又比如下面這個(gè)例子,iOS 8 開始提供的模糊特效 UIBlurEffectView: 整個(gè)模糊過程分為多步: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)的行為,是通過 CALayer 的 shouldRasterize 光柵化操作實(shí)現(xiàn)的。 shouldRasterize 光柵化
開啟光柵化后,會(huì)觸發(fā)離屏渲染,Render Server 會(huì)強(qiáng)制將 CALayer 的渲染位圖結(jié)果 bitmap 保存下來,這樣下次再需要渲染時(shí)就可以直接復(fù)用,從而提高效率。 而保存的 bitmap 包含 layer 的 subLayer、圓角、陰影、組透明度 group opacity 等,所以如果 layer 的構(gòu)成包含上述幾種元素,結(jié)構(gòu)復(fù)雜且需要反復(fù)利用,那么就可以考慮打開光柵化。 圓角、陰影、組透明度等會(huì)由系統(tǒng)自動(dòng)觸發(fā)離屏渲染,那么打開光柵化可以節(jié)約第二次及以后的渲染時(shí)間。而多層 subLayer 的情況由于不會(huì)自動(dòng)觸發(fā)離屏渲染,所以相比之下會(huì)多花費(fèi)第一次離屏渲染的時(shí)間,但是可以節(jié)約后續(xù)的重復(fù)渲染的開銷。 不過使用光柵化的時(shí)候需要注意以下幾點(diǎn):
圓角的離屏渲染通常來講,設(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 屬性):
如果只是設(shè)置了 cornerRadius 而沒有設(shè)置 masksToBounds,由于不需要疊加裁剪,此時(shí)是并不會(huì)觸發(fā)離屏渲染的。而當(dāng)設(shè)置了裁剪屬性的時(shí)候,由于 masksToBounds 會(huì)對(duì) layer 以及所有 subLayer 的 content 都進(jìn)行裁剪,所以不得不觸發(fā)離屏渲染。
所以,Texture 也提出在沒有必要使用圓角裁剪的時(shí)候,盡量不去觸發(fā)離屏渲染而影響效率: 離屏渲染的具體邏輯剛才說了圓角加上 masksToBounds 的時(shí)候,因?yàn)?masksToBounds 會(huì)對(duì) layer 上的所有內(nèi)容進(jìn)行裁剪,從而誘發(fā)了離屏渲染,那么這個(gè)過程具體是怎么回事呢,下面我們來仔細(xì)講一下。 圖層的疊加繪制大概遵循“畫家算法”,在這種算法下會(huì)按層繪制,首先繪制距離較遠(yuǎn)的場(chǎng)景,然后用繪制距離較近的場(chǎng)景覆蓋較遠(yuǎn)的部分。 在普通的 layer 繪制中,上層的 sublayer 會(huì)覆蓋下層的 sublayer,下層 sublayer 繪制完之后就可以拋棄了,從而節(jié)約空間提高效率。所有 sublayer 依次繪制完畢之后,整個(gè)繪制過程完成,就可以進(jìn)行后續(xù)的呈現(xiàn)了。假設(shè)我們需要繪制一個(gè)三層的 sublayer,不設(shè)置裁剪和圓角,那么整個(gè)繪制過程就如下圖所示: 而當(dāng)我們?cè)O(shè)置了 cornerRadius 以及 masksToBounds 進(jìn)行圓角 + 裁剪時(shí),如前文所述,masksToBounds 裁剪屬性會(huì)應(yīng)用到所有的 sublayer 上。這也就意味著所有的 sublayer 必須要重新被應(yīng)用一次圓角+裁剪,這也就意味著所有的 sublayer 在第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer 中等待下一輪圓角+裁剪,這也就誘發(fā)了離屏渲染,具體過程如下: 實(shí)際上不只是圓角+裁剪,如果設(shè)置了透明度+組透明( 避免圓角離屏渲染除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎? 由于剛才我們提到,圓角引起離屏渲染的本質(zhì)是裁剪的疊加,導(dǎo)致 masksToBounds 對(duì) layer 以及所有 sublayer 進(jìn)行二次處理。那么我們只要避免使用 masksToBounds 進(jìn)行二次處理,而是對(duì)所有的 sublayer 進(jìn)行預(yù)處理,就可以只進(jìn)行“畫家算法”,用一次疊加就完成繪制。 那么可行的實(shí)現(xiàn)方法大概有下面幾種:
觸發(fā)離屏渲染原因的總結(jié)總結(jié)一下,下面幾種情況會(huì)觸發(fā)離屏渲染:
不過,需要注意的是,重寫 6. 自測(cè)題目一般來說做點(diǎn)題才能加深理解和鞏固,所以這里從文章里簡(jiǎn)單提煉了一些,希望能幫到大家:
參考文獻(xiàn):
![]()
|
|