一窝稿、FPS
1凿掂、概念
FPS(Frames Per Second),是指畫面每秒傳輸幀數(shù)庄萎。通俗來講就是指動(dòng)畫或視頻的畫面數(shù),每秒鐘幀數(shù)越多援奢,所顯示的動(dòng)作就會(huì)越流暢。通常集漾,要避免動(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)行同步督勺,顯示器(或者其他硬件)會(huì)用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號(hào)斤贰。當(dāng)電子槍換到新的一行,準(zhǔn)備進(jìn)行掃描時(shí)荧恍,顯示器會(huì)發(fā)出一個(gè)水平同步信號(hào)(horizonal synchronization),簡(jiǎn)稱 HSync摹菠;而當(dāng)一幀畫面繪制完成后,電子槍回復(fù)到原位次氨,準(zhǔn)備畫下一幀前,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization)糟需,簡(jiǎn)稱 VSync谷朝。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變圆凰。
計(jì)算機(jī)系統(tǒng)中 CPU、GPU挑童、顯示器是以上圖這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU站叼,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(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)行繪制,這樣就會(huì)導(dǎo)致屏幕撕裂(Screen Tearing)话告。這會(huì)使得屏幕中一部分顯示的是上一幀的內(nèi)容,另一部分顯示的是下一幀的內(nèi)容超棺。
如果顯示器的刷新頻率與 GPU 的渲染速度完全相同呵燕,應(yīng)該就會(huì)解決屏幕撕裂的問題了吧?其實(shí)并不是氧苍。顯示器從 GPU 拷貝幀的過程依然需要消耗一定的時(shí)間,如果屏幕在拷貝圖像時(shí)刷新让虐,仍然會(huì)導(dǎo)致屏幕撕裂問題罢荡。
4、緩沖區(qū)
引入緩沖區(qū)可以有效地緩解屏幕撕裂区赵,也就是同時(shí)使用一個(gè)幀緩沖區(qū)(frame buffer)和一個(gè)或者多個(gè)后備緩沖區(qū)(back buffer),在每次顯示器請(qǐng)求內(nèi)容時(shí)笼才,都會(huì)從幀緩沖區(qū)中取出圖像然后渲染。
在最簡(jiǎn)單的情況下骡送,幀緩沖區(qū)只有一個(gè),這時(shí)幀緩沖區(qū)的讀取和刷新都都會(huì)有比較大的效率問題摔踱。為了解決效率問題,顯示系統(tǒng)通常會(huì)引入兩個(gè)緩沖區(qū)派敷,即雙緩沖機(jī)制。在這種情況下般眉,GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取甸赃,當(dāng)下一幀渲染好后,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器埠对。如此一來效率會(huì)有很大的提升。
雖然緩沖區(qū)可以減緩這些問題貌笨,但是卻不能解決;如果后備緩沖區(qū)繪制完成襟沮,而幀緩沖區(qū)的圖像沒有被渲染,后備緩沖區(qū)中的圖像就會(huì)覆蓋幀緩沖區(qū)膀跌,仍然會(huì)導(dǎo)致屏幕撕裂。
5捅伤、 V-Sync
垂直同步(Vertical synchronization)巫玻,簡(jiǎn)稱 V-Sync ,主要作用就是保證只有在幀緩沖區(qū)中的圖像被渲染之后仍秤,后備緩沖區(qū)中的內(nèi)容才可以被拷貝到幀緩沖區(qū)中。
在 VSync 信號(hào)到來后徒扶,系統(tǒng)圖形服務(wù)會(huì)通過 CADisplayLink 等機(jī)制通知 App,App 主線程開始在 CPU 中計(jì)算顯示內(nèi)容导坟,比如視圖的創(chuàng)建、布局計(jì)算惫周、圖片解碼、文本繪制等康栈。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去喷橙,由 GPU 進(jìn)行變換贰逾、合成、渲染疙剑。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去践叠,等待下一次 VSync 信號(hào)到來時(shí)顯示到屏幕上。由于垂直同步的機(jī)制禁灼,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交弄捕,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示察藐,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變舟扎。這就是界面卡頓的原因。
從上面的圖中可以看到睹限,CPU 和 GPU 不論哪個(gè)阻礙了顯示流程,都會(huì)造成掉幀現(xiàn)象羡疗。所以開發(fā)時(shí),也需要分別對(duì) CPU 和 GPU 壓力進(jìn)行評(píng)估和優(yōu)化叨恨。
三、CPU vs GPU
1秉颗、CPU(中央處理器)
1、對(duì)象創(chuàng)建
對(duì)象的創(chuàng)建會(huì)分配內(nèi)存蚕甥、調(diào)整屬性栋荸、甚至還有讀取文件等操作凭舶,比較消耗 CPU 資源。盡量用輕量的對(duì)象代替重量的對(duì)象帅霜,可以對(duì)性能有所優(yōu)化。
通過 Storyboard 創(chuàng)建視圖對(duì)象時(shí)义屏,其資源消耗會(huì)比直接通過代碼創(chuàng)建對(duì)象要大非常多蜂大。
2、對(duì)象調(diào)整
對(duì)象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方奶浦。對(duì) UIView 的屬性進(jìn)行調(diào)整時(shí),消耗的資源要遠(yuǎn)大于一般的屬性澳叉。對(duì)此應(yīng)該盡量減少不必要的屬性修改。
當(dāng)視圖層次調(diào)整時(shí)成洗,UIView、CALayer 之間會(huì)出現(xiàn)很多方法調(diào)用與通知充包,所以在優(yōu)化性能時(shí),應(yīng)該盡量避免調(diào)整視圖層次基矮、添加和移除視圖冠场。
3、布局計(jì)算
視圖布局的計(jì)算是 App 中最為常見的消耗 CPU 資源的地方碴裙,如果能在后臺(tái)線程提前計(jì)算好視圖布局、并且對(duì)視圖布局進(jìn)行緩存舔株,那么這個(gè)地方基本就不會(huì)產(chǎn)生性能問題了。
4督笆、Autolayout
Autolayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率咕缎,但是 Autolayout 對(duì)于復(fù)雜視圖來說常常會(huì)產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長(zhǎng)凭豪,Autolayout 帶來的 CPU 消耗會(huì)呈指數(shù)級(jí)上升焙蹭。具體數(shù)據(jù)可以看這個(gè)文章:http://pilky.me/36/孔厉。 如果你不想手動(dòng)調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性)撰豺,或者使用 ComponentKit拼余、AsyncDisplayKit 等框架。
5匙监、文本計(jì)算
如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等)凡橱,文本的寬高計(jì)算會(huì)占用很大一部分資源稼钩,并且不可避免。如果你對(duì)文本顯示沒有特殊要求坝撑,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:]
來計(jì)算文本寬高,用 -[NSAttributedString drawWithRect:options:context:]
來繪制文本绍载。盡管這兩個(gè)方法性能不錯(cuò)滔蝉,但仍舊需要放到后臺(tái)線程進(jìn)行以避免阻塞主線程塔沃。
2、GPU(圖形處理器)
相對(duì)于 CPU 來說螃概,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形),應(yīng)用變換(transform)吊洼、混合并渲染,然后輸出到屏幕上冒窍。
1、offscreen rendering(離屏渲染)
這發(fā)生在當(dāng)不能直接在屏幕上繪制综液,并且必須繪制到離屏圖片的上下文中的時(shí)候。離屏繪制發(fā)生在基于CPU或者是GPU的渲染檩奠,或者是為離屏圖片分配額外內(nèi)存,以及切換繪制上下文附帽,這些都會(huì)降低GPU性能。對(duì)于特定圖層效果的使用乞而,比如圓角慢显,圖層遮罩,陰影或者是圖層光柵化都會(huì)強(qiáng)制Core Animation提前渲染圖層的離屏繪制荚藻。
2、視圖的混合
當(dāng)多個(gè)視圖(或者說 CALayer)重疊在一起顯示時(shí)应狱,GPU 會(huì)首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復(fù)雜除嘹,混合的過程也會(huì)消耗很多 GPU 資源岸蜗。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次璃岳,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無(wú)用的 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、如何檢測(cè)離屏渲染
1粟关、模擬器
Simulator -> Debug -> Color Off-screen Rendered
離屏渲染的圖層會(huì)變成黃色
2、 Instruments – Core Animation
3誊役、Instruments – OpenGL ES
5谷市、觸發(fā)離屏渲染的屬性
1击孩、cornerRadius+masksToBounds
首先設(shè)置圓角最簡(jiǎn)單的方法是調(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
稠鼻,不會(huì)觸發(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呜魄、繪制像素到屏幕上