屏幕的顯示原理
CRT電子槍按照圖片上的方式近她,從上到下、從左到右的方式一行行掃描逢渔,掃描完成之后顯示器就會(huì)顯示一幀的畫面递雀。隨后電子槍會(huì)回到初始位置繼續(xù)下一次掃描(就是黃色虛線部分)项棠。
這里涉及到兩個(gè)概念:水平同步信號(hào)(HSync),垂直同步信號(hào)(VSync)
HSync:當(dāng)電子槍換到新的一行悲雳,準(zhǔn)備開始掃描,顯示器會(huì)發(fā)出一個(gè)HSync
VSync:當(dāng)一幀畫面繪制完成后香追,電子槍回復(fù)到原位合瓢,準(zhǔn)備畫下一幀前,顯示器會(huì)發(fā)出一個(gè)VSync(就是黃色虛線部分)
我們都知道透典,顯示器通常以固定頻率進(jìn)行刷新晴楔,刷新頻率是每秒60幀,平均每幀就16.67ms峭咒。這個(gè)刷新頻率就是 VSync 信號(hào)產(chǎn)生的頻率税弃。
視圖渲染
UIKit使我們常用的框架
Core Animation翻譯為核心動(dòng)畫,它為圖形渲染和動(dòng)畫提供了基礎(chǔ)凑队,它依賴于OpenGL ES做GPU渲染则果,Core Graphics做CPU渲染。
Graphics Hardware是圖形硬件.
由上圖可知漩氨,要在屏幕上顯示視圖西壮,需要CPU和GPU一起協(xié)作,CPU計(jì)算好顯示的內(nèi)容提交到GPU叫惊,GPU渲染完成后將結(jié)果放到幀緩存區(qū)款青,隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示霍狰。
iOS使用的是雙緩沖機(jī)制抡草。即GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi)(前幀緩存),讓視頻控制器讀取蔗坯,當(dāng)下一幀渲染好后渠牲,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器(后幀緩存)。當(dāng)你視頻控制器已經(jīng)讀完一幀步悠,準(zhǔn)備讀下一幀的時(shí)候签杈,GPU 會(huì)等待顯示器的 VSync 信號(hào)發(fā)出后,前幀緩存和后幀緩存會(huì)瞬間切換,后幀緩存會(huì)變成新的前幀緩存答姥,同時(shí)舊的前幀緩存會(huì)變成新的后幀緩存铣除。
渲染流程
1.Commit Transaction:通過CPU創(chuàng)建繪制視圖,提交會(huì)話鹦付,包括自己和子數(shù)的layout得狀態(tài)尚粘,圖片的解碼和格式轉(zhuǎn)換等等。詳細(xì)可見下面圖片
2.Render Server:解析提交的子樹狀態(tài)敲长,生成繪制指令
3.GPU執(zhí)行繪制指令郎嫁,進(jìn)行渲染。
4.顯示渲染后的幀緩存的數(shù)據(jù)祈噪。
Commit Transaction
Commit Transaction - Layout(布局)
調(diào)用layoutSubviews方法泽铛。
調(diào)用addSubview方法
填充內(nèi)容,數(shù)據(jù)庫查詢
通常會(huì)造成CPU或者I/O瓶頸
Commit Transaction - Display(顯示)
通過drawRect:繪制內(nèi)容
String繪制
通常會(huì)造成CPU或者內(nèi)存瓶頸
Commit Transaction - Prepare(準(zhǔn)備提交)
圖片解碼辑鲤,把圖片從PNG或JPEG等格式中解壓出來盔腔,得到像素?cái)?shù)據(jù)
圖片格式轉(zhuǎn)換,如果GPU不支持這種顏色格式月褥,CPU需要進(jìn)行格式轉(zhuǎn)換
比如應(yīng)用中有一些從網(wǎng)絡(luò)下載的圖片弛随,而GPU恰好不支持這個(gè)格式,這就需要CPU預(yù)先進(jìn)行格式轉(zhuǎn)化宁赤。
Commit Transaction - Commit(提交)
打包layers并且提交到Render Server中
遞歸提交子樹的layers
如果子樹很復(fù)雜舀透,對CPU消耗很大
卡頓產(chǎn)生的原因
在 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í)葵擎,也需要分別對 CPU 和 GPU 壓力進(jìn)行評估和優(yōu)化谅阿。
CPU 資源消耗原因
1.對象創(chuàng)建:對象的創(chuàng)建會(huì)分配內(nèi)存、調(diào)整屬性酬滤、甚至還有讀取文件等操作签餐,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象盯串,可以對性能有所優(yōu)化氯檐。比如用CALayer代替UIView,用CATextLayer代替UILabel。另外嘴脾,通過 Storyboard 創(chuàng)建視圖對象時(shí),其資源消耗會(huì)比直接通過代碼創(chuàng)建對象要大非常多
2.對象銷毀:對象的銷毀雖然消耗資源不多蔬墩,但累積起來也是不容忽視的译打。通常當(dāng)容器類持有大量對象時(shí),其銷毀時(shí)的資源消耗就非常明顯拇颅。同樣的奏司,如果對象可以放到后臺(tái)線程去釋放,那就挪到后臺(tái)線程去
3.對象調(diào)整:當(dāng)視圖層次調(diào)整時(shí)樟插,UIView韵洋、CALayer 之間會(huì)出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí)黄锤,應(yīng)該盡量避免調(diào)整視圖層次搪缨、添加和移除視圖。
4.布局計(jì)算:視圖布局的計(jì)算是 App 中最為常見的消耗 CPU 資源的地方鸵熟。如果能在后臺(tái)線程提前計(jì)算好視圖布局副编、并且對視圖布局進(jìn)行緩存,那么這個(gè)地方基本就不會(huì)產(chǎn)生性能問題了流强。
5.Autolayout:但是 Autolayout 對于復(fù)雜視圖來說常常會(huì)產(chǎn)生嚴(yán)重的性能問題痹届。隨著視圖數(shù)量的增長,Autolayout 帶來的 CPU 消耗會(huì)呈指數(shù)級上升打月。具體數(shù)據(jù)可以看這個(gè)文章:http://pilky.me/36/队腐。
6.文本計(jì)算:如果一個(gè)界面中包含大量文字,文本的寬高計(jì)算會(huì)占用很大一部分資源奏篙,并且不可避免柴淘。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計(jì)算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本悠就。另外這兩個(gè)操作盡可能得在后臺(tái)操作千绪。
7.文本渲染:屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView梗脾,在底層都是通過 CoreText 排版荸型、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel炸茧、UITextView 等)瑞妇,其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí)梭冠,CPU 的壓力會(huì)非常大辕狰。對此解決方案只有一個(gè),那就是自定義文本控件控漠,用 TextKit 或最底層的 CoreText 對文本異步繪制蔓倍。盡管這實(shí)現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢也非常大盐捷,CoreText 對象創(chuàng)建好后偶翅,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整 UILabel 大小時(shí)算一遍碉渡、UILabel 繪制時(shí)內(nèi)部再算一遍)聚谁;CoreText 對象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染滞诺。
8.圖片的解碼:當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí)形导,圖片數(shù)據(jù)并不會(huì)立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去习霹,并且 CALayer 被提交到 GPU 前朵耕,CGImage 中的數(shù)據(jù)才會(huì)得到解碼。這一步是發(fā)生在主線程的淋叶,并且不可避免憔披。如果想要繞開這個(gè)機(jī)制,常見的做法是在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中爸吮,然后從 Bitmap 直接創(chuàng)建圖片芬膝。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個(gè)功能。例如:可見下面 圖-8
9.圖像的繪制:圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中形娇,然后從畫布創(chuàng)建圖片并顯示這樣一個(gè)過程锰霜。這個(gè)最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的桐早,所以圖像的繪制可以很容易的放到后臺(tái)線程進(jìn)行癣缅。詳情也可以見 圖-8
10.圖片的格式:把圖片從PNG或JPEG等格式中解壓出來厨剪,得到像素?cái)?shù)據(jù),友存,如果GPU不支持這種顏色格式祷膳,CPU需要進(jìn)行格式轉(zhuǎn)換,所以屡立,圖片要用GPU支持的圖片數(shù)據(jù)格式直晨。
GPU 資源消耗原因
相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形)膨俐,應(yīng)用變換(transform)勇皇、混合并渲染雕凹,然后輸出到屏幕上冀宴。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類习贫。
1.紋理的渲染:所有的 Bitmap乳愉,包括圖片兄淫、文本、柵格化的內(nèi)容蔓姚,最終都要由內(nèi)存提交到顯存捕虽,綁定為 GPU Texture。不論是提交到顯存的過程赂乐,還是 GPU 調(diào)整和渲染 Texture 的過程薯鳍,都要消耗不少 GPU 資源咖气。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動(dòng)時(shí))挨措,CPU 占用率很低,GPU 占用非常高崩溪,界面仍然會(huì)掉幀浅役。避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示伶唯。
當(dāng)圖片過大觉既,超過 GPU 的最大紋理尺寸時(shí),圖片需要先由 CPU 進(jìn)行預(yù)處理乳幸,這對 CPU 和 GPU 都會(huì)帶來額外的資源消耗瞪讼。目前來說,iPhone 4S 以上機(jī)型粹断,紋理尺寸上限都是 4096x4096符欠,更詳細(xì)的資料可以看這里:iosres.com。所以瓶埋,盡量不要讓圖片和視圖的大小超過這個(gè)值希柿。
2.視圖的混合 (Composing):當(dāng)出現(xiàn)重疊的UIView或者CALayer的時(shí)候诊沪,GPU會(huì)去計(jì)算混合部分的像素,所以曾撤,盡可能少的設(shè)置alpha,盡可能多的設(shè)置背景顏色為純色的背景端姚。網(wǎng)上的人說要設(shè)置 opaque = true,但是我個(gè)人認(rèn)為設(shè)置backgroundColor更為有效。
3.圖形的生成:CALayer 的 border挤悉、圓角渐裸、陰影、遮罩(mask)尖啡,CASharpLayer 的矢量圖形顯示橄仆,通常會(huì)觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中衅斩。當(dāng)一個(gè)列表視圖中出現(xiàn)大量圓角的 CALayer盆顾,并且快速滑動(dòng)時(shí),可以觀察到 GPU 資源已經(jīng)占滿畏梆,而 CPU 資源消耗很少您宪。這時(shí)界面仍然能正常滑動(dòng)奠涌,但平均幀數(shù)會(huì)降到很低宪巨。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性溜畅,但這會(huì)把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去捏卓。對于只需要圓角的某些場合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果慈格。最徹底的解決辦法怠晴,就是把需要顯示的圖形在后臺(tái)線程繪制為圖片,避免使用圓角浴捆、陰影蒜田、遮罩等屬性。
離屏渲染
離屏渲染指的是GPU在當(dāng)前屏幕緩存區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作选泻。
我們現(xiàn)在看看正常的渲染管道:
1.Command Buffer:OpenGL提交一個(gè)渲染指令給Command Buffer冲粤,然后進(jìn)行GPU渲染。
2.Tiler:調(diào)用頂點(diǎn)著色器页眯,把頂點(diǎn)數(shù)據(jù)進(jìn)行分塊(Tiling)
3.ParameterBuffer:接受分塊完畢的tile和對應(yīng)的渲染參數(shù)
4.Renderer:調(diào)用片元著色器梯捕,進(jìn)行像素渲染
5.Render Buffer: 緩沖區(qū),存儲(chǔ)渲染完畢的像素
離屏渲染的渲染管道(Mask)
1.在第一條渲染管道中窝撵,GPU創(chuàng)建了一個(gè)新的緩存區(qū)來存儲(chǔ)相機(jī)紋理(Texture)的渲染結(jié)果傀顾。
2.在第二條渲染管道中,GPU創(chuàng)建了一個(gè)新的緩存區(qū)來存儲(chǔ)藍(lán)色的蒙版(layer)的渲染結(jié)果忿族。
記住锣笨,前面的兩個(gè)渲染結(jié)果并沒有直接放到Render Buffer中蝌矛,而是臨時(shí)保存
3.在第三條渲染管道中,才把前面的兩個(gè)渲染結(jié)果取出來错英,合并組合入撒,放到Render Buffer中
由此可見,離屏渲染的消耗主要提現(xiàn)在兩個(gè)方法:
1.創(chuàng)建新的緩存區(qū)
2.上下文的切換椭岩。先是從當(dāng)前屏幕切換到離屏茅逮,等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕判哥。而上下文環(huán)境的切換是要付出很大代價(jià)的献雅。
常見的離屏渲染操作有哪些?
1.shouldRasterize(光柵化):這是一個(gè)手動(dòng)開啟離屏渲染的操作塌计,開啟光柵化是將一個(gè)layer預(yù)先渲染成位圖(bitmap)挺身,然后加入緩存中,當(dāng)需要的時(shí)候再從緩存中拿出來锌仅,如果光柵化的元素在100ms內(nèi)沒有被使用就會(huì)被移除章钾,所以,光柵化適用于靜態(tài)內(nèi)容的視圖热芹,也就是內(nèi)部結(jié)構(gòu)和內(nèi)容不發(fā)生變化的視圖贱傀,當(dāng)視圖的內(nèi)容和結(jié)構(gòu)一直在發(fā)生變化,就不應(yīng)該開啟光柵化伊脓,因?yàn)樗鼤?huì)一直創(chuàng)建緩存府寒。
2.masks(遮罩):單獨(dú)設(shè)置cornerRadius或者masksToBounds = true,并不會(huì)引起離屏渲染报腔,他們的合體才會(huì)引起離屏渲染株搔。可以選擇VVebo的做法榄笙,在要添加圓角的視圖上再疊加一個(gè)部分透明的視圖邪狞,只對圓角部分進(jìn)行遮擋祷蝌,遮擋的部分背景與周圍背景相同茅撞。但最徹底的方法還是在后臺(tái)線程中利用CoreGraphic來繪制圖片。另外巨朦,這里說一下UILabel和UITextView設(shè)置圓角的方法米丘,按照下面這么做不會(huì)產(chǎn)生離屏渲染。
label.layer.backgroundColor= aColor
label.layer.cornerRadius=5
3.shadows(陰影):可以通過指定陰影路徑糊啡,避免離屏渲染拄查。imgView.layer.shadowPath =UIBezierPath(rect: imgView.bounds).CGPath
4.EdgeAntialiasing(抗鋸齒):這個(gè)功能貌似在 iOS 8 和 iOS 9 上并不會(huì)觸發(fā)離屏渲染,對性能也沒有什么影響棚蓄,也許這個(gè)功能已經(jīng)被優(yōu)化了堕扶。
5.-(void)drawRect (CPU離屏渲染):如果我們重 寫了drawRect方法碍脏,并且使用Core Graphics的技術(shù)進(jìn)行了繪制操作,就涉及到了CPU渲染稍算。但是我們應(yīng)該盡量避免這種方法典尾,因?yàn)槿绻褂眠@個(gè)方法不恰當(dāng)?shù)脑挄?huì)使內(nèi)存暴漲,這個(gè)在內(nèi)存惡鬼drawRect中做出了詳細(xì)解釋糊探〖毓。總之,能避免重寫drawRect方法就盡可能避免科平。
參考文獻(xiàn)
http://www.reibang.com/p/748f9abafff8