客戶端的開發(fā),無非離不開數(shù)據(jù)和展示,而展示這個方面,首當其沖的就是視圖、動畫的渲染,切換等等。而且在用戶的使用中,UI 是這個 APP 的門面,無論功能有多強大,體驗不好也是無法留住用戶的。
硬件圖像顯示的基本原理

首先從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱 HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率。盡管現(xiàn)在的設備大都是液晶顯示屏了,但原理仍然沒有變。

通常來說,計算機系統(tǒng)中 CPU、GPU、顯示器是以上面這種方式協(xié)同工作的。CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
在最簡單的情況下,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)(后面會講到緩沖區(qū)以及幀緩沖區(qū)的概念)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制。在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區(qū)內(nèi),讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。
雙緩沖雖然能解決效率問題,但會引入一個新的問題。當視頻控制器還未讀取完成時,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象,如下圖:

為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。
那么目前主流的移動設備是什么情況呢?從網(wǎng)上查到的資料可以知道,iOS 設備會始終使用雙緩存,并開啟垂直同步。而安卓設備直到 4.1 版本,Google 才開始引入這種機制,目前安卓系統(tǒng)是三緩存+垂直同步。
OpenGL ES
OpenGL ES 是一種軟件技術(shù),上文講到計算機中 CPU GPU 協(xié)同工作完成渲染,在手機端,正是 OpenGL ES 橫跨在兩個處理器之間,協(xié)調(diào)兩個區(qū)域之間的數(shù)據(jù)交換。
OpenGL ES 為了提升渲染的性能,為兩個內(nèi)存區(qū)域間的數(shù)據(jù)交換定義了緩沖區(qū)的概念 (buffers) 。緩沖區(qū)是指 GPU 能夠控制和管理的連續(xù) RAM 。程序從 CPU 的內(nèi)存復制數(shù)據(jù)到 OpenGL ES 的緩沖區(qū)。通過獨占緩沖區(qū),GPU 能夠盡可能以有效的方式讀寫內(nèi)存。 GPU 把它處理數(shù)據(jù)的能力異步地應用在緩沖區(qū)上,意味著 GPU 使用緩沖區(qū)中的數(shù)據(jù)工作的同時,運行在 CPU 中的程序可以繼續(xù)執(zhí)行。
GPU 需要知道內(nèi)存中的哪個位置來存儲渲染出來的 2D 圖像像素數(shù)據(jù),接收渲染結(jié)果的緩沖區(qū)稱為幀緩沖區(qū) (frame buffer)。渲染指令會在適當?shù)臅r候替換幀緩沖區(qū)中的內(nèi)容,OpenGL ES 會根據(jù)特定平臺硬件配置和功能設置數(shù)據(jù)類型和偏移。通常來說,渲染結(jié)果可以存儲到任意數(shù)量的 frame buffer 中。上面提到的雙緩沖的兩個緩沖稱之為前幀緩沖區(qū) (front frame buffer)和后幀緩沖區(qū) (back frame buffer)。
在 OpenGL ES 中,所有的圖像都可以由點,線段和三角形構(gòu)成,所以 OpenGL ES 只渲染這三種圖形。在接收到一些頂點數(shù)據(jù)后,經(jīng)過頂點著色器 (vertex shader)處理,裝配輸出給片元著色器 (fragment shader),再經(jīng)過一些操作最終輸出給幀緩沖區(qū)。什么是片元呢?通常在頂點著色器輸出幾何圖形數(shù)據(jù)后,會進行光柵化 (rasterizing)將這些形狀數(shù)據(jù)轉(zhuǎn)換為幀緩存中的顏色像素,而每一個顏色像素就叫做片元 (fragment)。
下圖為整個 OpenGL ES 的繪制管道:

iOS 設備與 OpenGL ES
每一個 iOS 原生用戶界面對象都有對應的 Core Animation Layer, layer 會保存所有繪制操作的結(jié)果。蘋果的 Core Animation 合成器使用 OpenGL ES 來盡可能高效地控制 GPU 、混合 layer 和切換幀緩沖區(qū)。圖形程序員經(jīng)常使用混合 (composite)來描述混合圖像來形成一個合成結(jié)果的過程。所有顯示的圖畫都是通過 Core Animation 合成器來完成的,因此最終都涉及 OpenGL ES 。
蘋果官方文檔描述了 iOS 設備圖形顯示的架構(gòu):

渲染的各個階段
在應用內(nèi)部有四個階段:
布局:在這個階段,程序設置 View/Layer 的層級信息,設置 layer 的屬性,如 frame,background color 等等。
創(chuàng)建 backing image:在這個階段程序會創(chuàng)建 layer 的 backing image,無論是通過 setContents 將一個 image 傳給 layer,還是通過 drawRect:或 drawLayer:inContext:來畫出來的。所以 drawRect:等函數(shù)是在這個階段被調(diào)用的。
準備:在這個階段,Core Animation 框架準備要渲染的 layer 的各種屬性數(shù)據(jù),以及要做的動畫的參數(shù),準備傳遞給 render server。同時在這個階段也會解壓要渲染的 image。(除了用 imageNamed:方法從 bundle 加載的 image 會立刻解壓之外,其他的比如直接從硬盤讀入,或者從網(wǎng)絡上下載的 image 不會立刻解壓,只有在真正要渲染的時候才會解壓)。
提交:在這個階段,Core Animation 打包 layer 的信息以及需要做的動畫的參數(shù),通過 IPC(inter-Process Communication)傳遞給 render server。
在應用外部有兩個階段:
當這些數(shù)據(jù)到達 render server 后,會被反序列化成 render tree。然后 render server 會做下面的兩件事:
根據(jù) layer 的各種屬性(如果是動畫的,會計算動畫 layer 的屬性的中間值),用 OpenGL 準備渲染。
渲染這些可視的 layer 到屏幕。
如果做動畫的話,最后的兩個步驟會一直重復知道動畫結(jié)束。
我們都知道 iOS 設備的屏幕刷新頻率是 60HZ。如果上面的這些步驟在一個刷新周期之內(nèi)無法做完(1/60s),就會造成掉幀。
資源消耗的原因和解決方案
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
紋理的渲染
所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096x4096,更詳細的資料可以看這里:iosres.com。所以,盡量不要讓圖片和視圖的大小超過這個值。
視圖的混合 (Composing)
當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當盡量減少視圖數(shù)量和層次,并在不透明的視圖里標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個視圖預先渲染為一張圖片來顯示。
圖形的生成。
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中。當一個列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時,可以觀察到 GPU 資源已經(jīng)占滿,而 CPU 資源消耗很少。這時界面仍然能正?;瑒?,但平均幀數(shù)會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對于只需要圓角的某些場合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。
離屏渲染
OpenGL 中,GPU 屏幕渲染有以下兩種方式:
On-Screen Rendering 意為當前屏幕渲染,指的是 GPU 的渲染操作是在當前用于顯示的屏幕緩沖區(qū)中進行。
Off-Screen Rendering 意為離屏渲染,指的是 GPU 在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作。
相比于當前屏幕渲染,離屏渲染的代價是很高的,主要體現(xiàn)在兩個方面:
創(chuàng)建新緩沖區(qū) 要想進行離屏渲染,首先要創(chuàng)建一個新的緩沖區(qū)。
上下文切換 離屏渲染的整個過程,需要多次切換上下文環(huán)境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當前屏幕。而上下文環(huán)境的切換是要付出很大代價的。
所以在圖形生成的步驟我們要盡可能的避免離屏渲染,或者開啟shouldRasterize屬性。
渲染優(yōu)化的注意點
根據(jù)上面的描述,簡單的匯總一些優(yōu)化渲染的注意點:
隱藏的繪制:catextlayer 和 uilabel 都是將 text 畫入 backing image 的。如果改了一個包含 text 的 view 的 frame 的話,text 會被重新繪制。
Rasterize:當使用 layer 的 shouldRasterize 的時候(記得設置適當?shù)?layer 的 rasterizationScale),layer 會被強制繪制到一個 offscreen image 上,并且會被緩存起來。這種方法可以用來緩存繪制耗時(比如有比較絢的效果)但是不經(jīng)常改的 layer,如果 layer 經(jīng)常變,就不適合用。
離屏繪制: 使用 Rounded corner, layer masks, drop shadows 的效果可以使用 stretchable images。比如實現(xiàn) rounded corner,可以將一個圓形的圖片賦值于 layer 的 content 的屬性。并且設置好 contentsCenter 和 contentScale 屬性。
Blending and Overdraw :如果一個 layer 被另一個 layer 完全遮蓋,GPU 會做優(yōu)化不渲染被遮蓋的 layer,但是計算一個 layer 是否被另一個 layer 完全遮蓋是很耗 cpu 的。將幾個半透明的 layer 的 color 融合在一起也是很消耗的。
我們要做的:
設置 view 的 backgroundColor 為一個固定的,不透明的 color。 如果一個 view 是不透明的,設置 opaque 屬性為 YES。(直接告訴程序這個是不透明的,而不是讓程序去計算) 這樣會減少 blending 和 overdraw。
如果使用 image 的話,盡量避免設置 image 的 alpha 為透明的,如果一些效果需要幾個圖片融合而成,就讓設計用一張圖畫好,不要讓程序在運行的時候去動態(tài)的融合。
這篇文章林林總總的描述了一些概念和注意點,在優(yōu)化時可以結(jié)合 Instrument 中的 Core Animation 和 GPU Driver 來進行測試。對于局部需要特別要求性能的地方可以嘗試 Facebook 開源的AsyncDisplayKit或者國內(nèi)大牛的YYKit。
以上就是這篇文章的全部內(nèi)容,上面大部分的內(nèi)容總結(jié)以及參考自一些書籍和文章:
《iOS 視圖、動畫渲染機制探究》 - 騰訊 Bugly
《iOS 保持界面流暢的技巧》 - ibireme
下面附上代碼:


