一、FPS
1、概念
FPS(Frames Per Second),是指畫面每秒傳輸幀數(shù)。通俗來講就是指動(dòng)畫或視頻的畫面數(shù),每秒鐘幀數(shù)越多,所顯示的動(dòng)作就會越流暢。通常,要避免動(dòng)作不流暢的最低是30,iOS的優(yōu)化極限則是60。
二、顯示
1、顯示原理
計(jì)算機(jī)顯示的流程大致可以描述為將圖像轉(zhuǎn)化為一系列像素點(diǎn)的排列然后打印在屏幕上,由圖像轉(zhuǎn)化為像素點(diǎn)的過程又可以稱之為光柵化,就是從矢量的點(diǎn)線面的描述,變成像素的描述。屏幕顯示圖像的原理如圖:

首先從過去的 CRT 顯示器原理說起。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。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變。

計(jì)算機(jī)系統(tǒng)中 CPU、GPU、顯示器是以上圖這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
2、iOS顯示
從軟件層面上,iOS借助Core Graohics,Core Animation,Core Image完成圖形的處理,它們又都是借助OpenGL ES來完成底層的工作,其結(jié)構(gòu)如下圖所示:

Display 的上一層便是圖形處理單元 GPU,GPU 是一個(gè)專門為圖形高并發(fā)計(jì)算而量身定做的處理單元。這也是為什么它能同時(shí)更新所有的像素,并呈現(xiàn)到顯示器上。它并發(fā)的本性讓它能高效的將不同紋理合成起來。因?yàn)樯婕暗礁鞣N圖形矩陣的計(jì)算,它跟CPU最直觀的區(qū)別在于浮點(diǎn)計(jì)算能力要超出CPU很多。所以在開發(fā)中,我們應(yīng)該盡量讓CPU負(fù)責(zé)主線程的UI調(diào)動(dòng),把圖形顯示相關(guān)的工作交給GPU來處理。
3、屏幕撕裂
生成圖像的設(shè)備(如GPU)與顯示圖像的設(shè)備(如顯示器)是分離的。顯示器的刷新頻率是固定的,而顯卡的生成圖像的頻率是變化的。當(dāng)GPU還在渲染下一幀圖像時(shí),顯示器卻已經(jīng)開始進(jìn)行繪制,這樣就會導(dǎo)致屏幕撕裂(Screen Tearing)。這會使得屏幕中一部分顯示的是上一幀的內(nèi)容,另一部分顯示的是下一幀的內(nèi)容。

如果顯示器的刷新頻率與 GPU 的渲染速度完全相同,應(yīng)該就會解決屏幕撕裂的問題了吧?其實(shí)并不是。顯示器從 GPU 拷貝幀的過程依然需要消耗一定的時(shí)間,如果屏幕在拷貝圖像時(shí)刷新,仍然會導(dǎo)致屏幕撕裂問題。
4、緩沖區(qū)
引入緩沖區(qū)可以有效地緩解屏幕撕裂,也就是同時(shí)使用一個(gè)幀緩沖區(qū)(frame buffer)和一個(gè)或者多個(gè)后備緩沖區(qū)(back buffer),在每次顯示器請求內(nèi)容時(shí),都會從幀緩沖區(qū)中取出圖像然后渲染。
在最簡單的情況下,幀緩沖區(qū)只有一個(gè),這時(shí)幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統(tǒng)通常會引入兩個(gè)緩沖區(qū),即雙緩沖機(jī)制。在這種情況下,GPU 會預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取,當(dāng)下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個(gè)緩沖器。如此一來效率會有很大的提升。
雖然緩沖區(qū)可以減緩這些問題,但是卻不能解決;如果后備緩沖區(qū)繪制完成,而幀緩沖區(qū)的圖像沒有被渲染,后備緩沖區(qū)中的圖像就會覆蓋幀緩沖區(qū),仍然會導(dǎo)致屏幕撕裂。
5、 V-Sync
垂直同步(Vertical synchronization),簡稱 V-Sync ,主要作用就是保證只有在幀緩沖區(qū)中的圖像被渲染之后,后備緩沖區(qū)中的內(nèi)容才可以被拷貝到幀緩沖區(qū)中。
在 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)容不變。這就是界面卡頓的原因。

從上面的圖中可以看到,CPU 和 GPU 不論哪個(gè)阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時(shí),也需要分別對 CPU 和 GPU 壓力進(jìn)行評估和優(yōu)化。
三、CPU vs GPU
1、CPU(中央處理器)
1、對象創(chuàng)建
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優(yōu)化。
通過 Storyboard 創(chuàng)建視圖對象時(shí),其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多。
2、對象調(diào)整
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方。對 UIView 的屬性進(jìn)行調(diào)整時(shí),消耗的資源要遠(yuǎn)大于一般的屬性。對此應(yīng)該盡量減少不必要的屬性修改。
當(dāng)視圖層次調(diào)整時(shí),UIView、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí),應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
3、布局計(jì)算
視圖布局的計(jì)算是 App 中最為常見的消耗 CPU 資源的地方,如果能在后臺線程提前計(jì)算好視圖布局、并且對視圖布局進(jìn)行緩存,那么這個(gè)地方基本就不會產(chǎn)生性能問題了。
4、Autolayout
Autolayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對于復(fù)雜視圖來說常常會產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升。具體數(shù)據(jù)可以看這個(gè)文章:http://pilky.me/36/。 如果你不想手動(dòng)調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
5、文本計(jì)算
如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計(jì)算會占用很大一部分資源,并且不可避免。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計(jì)算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本。盡管這兩個(gè)方法性能不錯(cuò),但仍舊需要放到后臺線程進(jìn)行以避免阻塞主線程。
2、GPU(圖形處理器)
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形),應(yīng)用變換(transform)、混合并渲染,然后輸出到屏幕上。
1、offscreen rendering(離屏渲染)
這發(fā)生在當(dāng)不能直接在屏幕上繪制,并且必須繪制到離屏圖片的上下文中的時(shí)候。離屏繪制發(fā)生在基于CPU或者是GPU的渲染,或者是為離屏圖片分配額外內(nèi)存,以及切換繪制上下文,這些都會降低GPU性能。對于特定圖層效果的使用,比如圓角,圖層遮罩,陰影或者是圖層光柵化都會強(qiáng)制Core Animation提前渲染圖層的離屏繪制。
2、視圖的混合
當(dāng)多個(gè)視圖(或者說 CALayer)重疊在一起顯示時(shí),GPU 會首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復(fù)雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成。
四、離屏渲染
1、概念
On-Screen Rendering:意為當(dāng)前屏幕渲染,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行。
Off-Screen Rendering:意為離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
2、離屏渲染原因
3、為何要避免離屏渲染
WWDC 2011 Understanding UIKit Rendering 指出一般導(dǎo)致圖形性能的問題大部分都出在了offscreen rendering,因此如果我們發(fā)現(xiàn)列表滾動(dòng)不流暢,動(dòng)畫卡頓等問題,就可以想想和找出我們哪部分代碼導(dǎo)致了大量的offscreen 渲染。
離屏渲染主要在兩個(gè)地方開銷較大:
1、創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染,首先要?jiǎng)?chuàng)建一個(gè)新的緩沖區(qū)。
2、上下文切換
離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。而上下文環(huán)境的切換是要付出很大代價(jià)的。
4、如何檢測離屏渲染
1、模擬器
Simulator -> Debug -> Color Off-screen Rendered
離屏渲染的圖層會變成黃色

2、 Instruments – Core Animation
3、Instruments – OpenGL ES
5、觸發(fā)離屏渲染的屬性
1、cornerRadius+masksToBounds
首先設(shè)置圓角最簡單的方法是調(diào)用cornerRadius,官方文檔中描述cornerRadius只作用于background color and border of the layer,所以如果有內(nèi)容需要設(shè)置 masksToBounds 為YES裁剪內(nèi)容。
view.layer.cornerRadius = 10.f;
eg
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.f, 20.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.cornerRadius = 50.f;
[self.view addSubview:view];
上文代碼并沒有觸發(fā)離屏渲染,所以結(jié)論:視圖設(shè)置圓角,如果沒有內(nèi)容,一般來說僅指定cornerRadius即可;如果有內(nèi)容,需指定masksToBounds,并進(jìn)行實(shí)際裁剪,從而產(chǎn)生離屏渲染。
產(chǎn)生離屏渲染的例子:
// 1、UIView添加子視圖,cornerRadius+masksToBounds+subview
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.f, 20.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.cornerRadius = 50.f;
view.layer.masksToBounds = YES;
[self.view addSubview:view];
UIView *subview = [[UIView alloc] initWithFrame:view.bounds];
subview.backgroundColor = [UIColor greenColor];
[view addSubview:subview];
// 2、UILabel添加標(biāo)題,cornerRadius+masksToBounds+backgroundColor+borderWidth
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(150, 50.f, 100.f, 100.f)];
label.backgroundColor = [UIColor blueColor];
label.text = @"UILabelUILabelUILabelUILabelUILabelUILabelUILabelUILabel";
label.numberOfLines = 0;
label.layer.borderWidth = 5.f;
label.layer.borderColor = [UIColor redColor].CGColor;
label.layer.cornerRadius = 50.f;
label.layer.masksToBounds = YES;
[self.view addSubview:label];
// 3、UIButton添加標(biāo)題,cornerRadius+masksToBounds+backgroundColor+title
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 100, 200, 40.f);
button.backgroundColor = [UIColor redColor];
[button setTitle:@"button" forState:UIControlStateNormal];
button.layer.cornerRadius = 20.f;
button.layer.masksToBounds = YES;
[self.view addSubview:button];
// 4、UIImageView添加圖片,cornerRadius+masksToBounds+backgroundColor+image
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 300.f, 100.f, 100.f)];
imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"jd"];
imageView.layer.cornerRadius = 50.f;
imageView.layer.masksToBounds = YES;
[self.view addSubview:imageView];
// 5、UITextView添加文字,cornerRadius+masksToBounds?+backgroundColor?+text
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 450.f, [UIScreen mainScreen].bounds.size.width, 100.f)];
textView.backgroundColor = [UIColor orangeColor];
textView.text = @"UITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextViewUITextView";
textView.layer.cornerRadius = 50.f;
// textView.layer.masksToBounds = YES;
[self.view addSubview:textView];
綜上所述,圓角圖形關(guān)于離屏渲染的優(yōu)化,不一定非得去寫貝塞爾曲線:
1、UIView,一般用來實(shí)現(xiàn)純色圓角視圖,盡量不要添加不透明的子視圖,設(shè)置layer.cornerRadius添加圓角,不設(shè)置masksToBounds即可避免離屏渲染
2、UILabel,一般用來實(shí)現(xiàn)圓角背景文本框或者有圓角邊框的文本框,與UIView相比較有些特殊,僅設(shè)置backgroundColor和layer.cornerRadius不顯示圓角,猜想backgroundColor是繪制在內(nèi)容上了,設(shè)置masksToBounds后可出現(xiàn)圓角且并沒有觸發(fā)離屏渲染。
實(shí)現(xiàn)圓角背景文本框:只要不添加border,不會觸發(fā)離屏渲染。
有圓角邊框的文本框,設(shè)置borderWidth觸發(fā)離屏渲染,取消backgroundColor和masksToBounds可避免離屏渲染。
3、UIButton,一般是要設(shè)置背景色和標(biāo)題,設(shè)置cornerRadius添加圓角,不設(shè)置masksToBounds即可避免離屏渲染
4、UIImageView,一般要設(shè)置圖片,設(shè)置cornerRadius添加圓角,設(shè)置masksToBounds裁剪內(nèi)容,不設(shè)置背景色即可避免離屏渲染
1、shadow
添加陰影觸發(fā)離屏渲染,設(shè)置shadowPath,提前告訴CoreAnimation要渲染的View的形狀,可避免觸發(fā)離屏渲染
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50.f, 50.f, 100, 100.f)];
view.backgroundColor = [UIColor redColor];
view.layer.shadowColor = [UIColor blueColor].CGColor;
view.layer.shadowOffset = CGSizeMake(10.f, 10.f);
view.layer.shadowOpacity = 1;
view.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.layer.bounds].CGPath;
[self.view addSubview:view];
參考:
1、腦洞大開:為啥幀率達(dá)到 60 fps 就流暢
2、提升 iOS 界面的渲染性能
3、CPU VS GPU
4、iOS 保持界面流暢的技巧
5、iOS圖形原理與離屏渲染
6、繪制像素到屏幕上