iOS視圖成像理論及優(yōu)化
CRT屏幕成像

CRT(陰極射線管)顯示器電子槍,電子槍從屏幕的左上角的第一行開始,從左至右逐行掃描,第一行掃描完后再從第二行的最左端開始至第二行的最右端,一直到掃描完整個(gè)屏幕后再從屏幕的左上角開始,這時(shí)就完成了一次對屏幕的刷新。
CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進(jìn)行同步,顯示器(或者其他硬件)會用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號。當(dāng)電子槍換到新的一行,準(zhǔn)備進(jìn)行掃描時(shí),顯示器會發(fā)出一個(gè)水平同步信號(horizonal synchronization),簡稱 HSync;而當(dāng)一幀畫面繪制完成后,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前,顯示器會發(fā)出一個(gè)垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進(jìn)行刷新,這個(gè)刷新率就是 VSync 信號產(chǎn)生的頻率。

計(jì)算機(jī)處理成像
計(jì)算機(jī)系統(tǒng)中 CPU、GPU、顯示器是協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照垂直同步信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。

- 所謂幀緩存,是指渲染的幀保存在的位置。使用內(nèi)置幀緩存,調(diào)用文件會更快,減少內(nèi)存使用。
- 在最簡單的情況下,幀緩沖區(qū)只有一個(gè),這時(shí)幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。
- 為了解決效率問題,顯示系統(tǒng)通常會引入兩個(gè)緩沖區(qū),即雙緩沖機(jī)制。
- 在這種情況下,GPU 會預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取,當(dāng)下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個(gè)緩沖器。如此一來效率會有很大的提升。
垂直同步
雙緩沖雖然能解決效率問題,但會引入一個(gè)新的問題。當(dāng)視頻控制器還未讀取完成時(shí),即屏幕內(nèi)容剛顯示一半時(shí),GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個(gè)緩沖區(qū)進(jìn)行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象,如下圖:

針對這個(gè)問題,GPU通常會做垂直同步,GPU 會等待顯示器的垂直同步信號發(fā)出后,才進(jìn)行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費(fèi)更多的計(jì)算資源,也會帶來部分延遲?,F(xiàn)在iOS 設(shè)備會始終使用雙緩存,并開啟垂直同步
界面卡頓的原因
在 VSync 信號到來后,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機(jī)制通知 App,App 主線程開始在 CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等。隨后 CPU 會將計(jì)算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時(shí)顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機(jī)會再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。
那么目前主流的移動設(shè)備是什么情況呢?從網(wǎng)上查到的資料可以知道,iOS 設(shè)備會始終使用雙緩存,并開啟垂直同步。而安卓設(shè)備直到 4.1 版本,Google 才開始引入這種機(jī)制,目前安卓系統(tǒng)是三緩存+垂直同步
屏幕渲染(GPU)
GPU屏幕渲染有以下兩種方式:
1.On-Screen Rendering
意為當(dāng)前屏幕渲染,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行。
2.Off-Screen Rendering
意為離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
離屏渲染為何卡?
Offscreen Render為什么卡頓?因?yàn)镺ffscreen Render需要更多的渲染通道,離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕,不同的渲染通道間切換需要耗費(fèi)一定的時(shí)間,這個(gè)時(shí)間內(nèi)GPU會閑置,當(dāng)通道達(dá)到一定數(shù)量,對性能也會有較大的影響
離屏渲染需慎重
混合圖層做動畫時(shí),GPU 會為每一幀(1/60s)重復(fù)合成所有的圖層。當(dāng)使用離屏渲染時(shí),GPU 第一次會混合所有圖層到一個(gè)基于新的紋理的位圖緩存上,然后使用這個(gè)紋理來繪制到屏幕上。當(dāng)這些圖層一起移動的時(shí)候,GPU 便可以復(fù)用這個(gè)位圖緩存,并且只需要做很少的工作。如果那些圖層改變了,GPU 需要重新創(chuàng)建位圖緩存。所以使用時(shí),還是需要慎重。
離屏渲染觸發(fā)條件
如果您在開發(fā)中不是專門做圖像處理的,請避免使用離屏渲染。shouldRasterize(光柵化)、masks(遮罩)、shadows(陰影)、edge antialiasing(抗鋸齒)、group opacity(不透明)都會觸發(fā)離屏繪制。具體如下:
- drawRect
- layer.shouldRasterize = true;
- 有mask或者是陰影(layer.masksToBounds, layer.shadow*);
- 3.1) shouldRasterize(光柵化)
- 3.2) masks(遮罩)
- 3.3) shadows(陰影)
- 3.4) edge antialiasing(抗鋸齒)
- 3.5) group opacity(不透明)
- Text(UILabel, CATextLayer, Core Text)...
離屏渲染何時(shí)用?
什么時(shí)候用離屏渲染呢? 當(dāng)你的渲染樹非常復(fù)雜(紋理及組合邏輯) 你可以強(qiáng)制離屏渲染緩存那些圖層,然后可以用緩存作為合成的結(jié)果放到屏幕上,通過設(shè)置 shouldRasterize 為 YES 來觸發(fā)這個(gè)行為
在觸發(fā)離屏繪制的同時(shí),會將光柵化后的內(nèi)容緩存起來,如果對應(yīng)的layer及其sublayers沒有發(fā)生改變,在下一幀的時(shí)候可以直接復(fù)用,光柵化相當(dāng)于是把GPU的操作轉(zhuǎn)到CPU上了,生成位圖緩存,直接讀取復(fù)用,這將在很大程度上提升渲染性能。但是rasterized layer(柵格化圖層)的空間是有限的,iOS大概有屏幕大小兩倍的空間來存儲 rasterized layer或是屏幕外緩沖區(qū)。
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
特殊的離屏渲染
特殊的“離屏渲染”方式:CPU渲染
Core Graphics 的繪制 API 會觸發(fā)離屏渲染,但不是那種 GPU 的離屏渲染。使用 Core Graphics 繪制 API 是在 CPU 上執(zhí)行,觸發(fā)的是 CPU 版本的離屏渲染。如果我們重寫了drawRect方法(使用CoreGraphics來實(shí)現(xiàn)的繪制,或使用CoreText[其實(shí)就是使用CoreGraphics]繪制)進(jìn)行操作,就涉及到了CPU渲染。整個(gè)渲染過程由CPU在App內(nèi)同步地完成,渲染得到的bitmap最后再交由GPU用于顯示,基本上使用時(shí)只是調(diào)用了一些向位圖緩存內(nèi)寫入一些二進(jìn)制信息的方法而已
總結(jié)
- RoundedCorner 在僅指定cornerRadius時(shí)不會觸發(fā)離屏渲染,僅適用于特殊情況:contents為 nil 或者contents不會遮擋背景色圓角;
- Shawdow 可以通過指定路徑來取消離屏渲染;
- Mask 無法取消離屏渲染;
任何時(shí)候優(yōu)先考慮避免觸發(fā)離屏渲染,無法避免時(shí)優(yōu)化方案有兩種:
- Rasterization:適用于靜態(tài)內(nèi)容的視圖,也就是內(nèi)部結(jié)構(gòu)和內(nèi)容不發(fā)生變化的視圖,對上面的所有效果而言,在實(shí)現(xiàn)成本以及性能上最均衡的。即使是動態(tài)變化的視圖,開啟 Rasterization 后能夠有效降低 GPU 的負(fù)荷,不過在動態(tài)視圖里是否啟用還是看 Instruments 的數(shù)據(jù)。
- 規(guī)避離屏渲染,用其他手法來模擬效果,混合圖層是個(gè)性能最好、耗能最少的通用優(yōu)化方案,尤其對于 rounded corer 和 mask。
比如:
這篇文章講了圓角的繪制對比:前臺重繪(消耗cup) 后臺重繪(消耗cup) 系統(tǒng)圓角(消耗gpu)
方案對比:
1.UI直接畫成圓角圖片
2.添加一個(gè)部分透明的視圖,只對圓角部分進(jìn)行遮擋

CPU 資源消耗原因和解決方案
對象創(chuàng)建
- 盡量推遲對象創(chuàng)建的時(shí)間,并把對象的創(chuàng)建分散到多個(gè)任務(wù)中去。盡管這實(shí)現(xiàn)起來比較麻煩,并且?guī)淼膬?yōu)勢并不多,但如果有能力做,還是要盡量嘗試一下。如果對象可以復(fù)用,并且復(fù)用的代價(jià)比釋放、創(chuàng)建新對象要小,那么這類對象應(yīng)當(dāng)盡量放到一個(gè)緩存池里復(fù)用。
- 在性能敏感的界面里,Storyboard 并不是一個(gè)好的技術(shù)選擇。
對象調(diào)整
- 當(dāng)視圖層次調(diào)整時(shí),UIView、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí),應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當(dāng)容器類持有大量對象時(shí),其銷毀時(shí)的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個(gè)小 Tip:把對象捕獲到 block 中,然后扔到后臺隊(duì)列去隨便發(fā)送個(gè)消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
布局計(jì)算
對這些屬性的調(diào)整非常消耗資源,所以盡量提前計(jì)算好布局,在需要時(shí)一次性調(diào)整好對應(yīng)屬性,而不要多次、頻繁的計(jì)算和調(diào)整這些屬性。
文本計(jì)算
- 如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計(jì)算會占用很大一部分資源,并且不可避免。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計(jì)算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本。盡管這兩個(gè)方法性能不錯(cuò),但仍舊需要放到后臺線程進(jìn)行以避免阻塞主線程。
- 如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對象,然后自己計(jì)算了,并且 CoreText 對象還能保留以供稍后繪制使用。
文本渲染
- 屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí),CPU 的壓力會非常大。對此解決方案只有一個(gè),那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實(shí)現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢也非常大,CoreText 對象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整 UILabel 大小時(shí)算一遍、UILabel 繪制時(shí)內(nèi)部再算一遍);CoreText 對象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染。
圖片的解碼
- 當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí),圖片數(shù)據(jù)并不會立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會得到解碼。這一步是發(fā)生在主線程的,并且不可避免。如果想要繞開這個(gè)機(jī)制,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個(gè)功能。
圖像的繪制
-
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創(chuàng)建圖片并顯示這樣一個(gè)過程。這個(gè)最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進(jìn)行。一個(gè)簡單異步繪制的過程大致如下(實(shí)際情況會比這個(gè)復(fù)雜得多,但原理基本一致):
- (void)display { dispatch_async(backgroundQueue, ^{ CGContextRef ctx = CGBitmapContextCreate(...); // draw in context... CGImageRef img = CGBitmapContextCreateImage(ctx); CFRelease(ctx); dispatch_async(mainQueue, ^{ layer.contents = img; }); }); }
GPU 資源消耗原因和解決方案
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形),應(yīng)用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
紋理的渲染
所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動時(shí)),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示。
視圖的混合 (Composing)
當(dāng)多個(gè)視圖(或者說 CALayer)重疊在一起顯示時(shí),GPU 會首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復(fù)雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成。當(dāng)然,這也可以用上面的方法,把多個(gè)視圖預(yù)先渲染為一張圖片來顯示。
圖形的生成。
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中。當(dāng)一個(gè)列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時(shí),可以觀察到 GPU 資源已經(jīng)占滿,而 CPU 資源消耗很少。這時(shí)界面仍然能正?;瑒?,但平均幀數(shù)會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對于只需要圓角的某些場合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。
AsyncDisplayKit
微博 Demo 性能優(yōu)化技巧
預(yù)排版
參考文章:
iOS保持界面流暢的技巧