圖像顯示原理
圖像顯示的大概流程:
- 程序運行從內(nèi)存中讀取數(shù)據(jù)
- 對圖片進行解壓得到像素數(shù)據(jù),若GPU不支持圖片的顏色格式,CPU需要進行格式轉(zhuǎn)換
- CoreText和CoreGraphics跟進文本內(nèi)容生成位圖
- 然后解壓后的數(shù)據(jù)或位圖通過GPU Bus上傳到GPU,GPU需要將每一個frame的紋理(位圖)合成在一起(一秒60次)。每一個紋理會占用VRAM(video RAM),所以需要給 GPU 同時保持紋理的數(shù)量做一個限制。GPU 在合成方面非常高效,但是某些合成任務卻比其他更復雜,并且 GPU在 16.7ms(1/60s)內(nèi)能做的工作也是有限的。
CPU的工作
- 對象的創(chuàng)建、調(diào)整和銷毀
- 布局計算
- 文本計算
- 文本渲染
- 圖片的解碼
- 圖像的繪制
GPU的工作
- 紋理的渲染
- 視圖合成
- 圖形生成
GPU顯示圖像
- CPU 計算好顯示內(nèi)容提交到 GPU
- GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)
- 隨后視頻控制器會按照VSync信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
????在最簡單的情況下,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制。在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區(qū)內(nèi),讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。
垂直同步機制
????當視頻控制器還未讀取完成時,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象。
????為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync)
????首先從過去的CRT顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變。
????當開啟垂直同步后,GPU 會等待顯示器的VSync信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。
卡頓的產(chǎn)生
????在VSync信號到來后,系統(tǒng)圖形服務會通過CADisplayLink等機制通知App,App主線程開始在CPU中計算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內(nèi)容不變。這就是界面卡頓的原因。
????從上面的圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化。
離屏渲染
GPU的渲染方式
-
On-Screen Rendering(當前屏幕渲染):指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區(qū)中進行。
enter image description here -
Off-Screen Rendering (離屏渲染),指的是GPU在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作。
enter image description here
????OffScreen Rendering 則多了一個步驟,GPU 會先創(chuàng)建一個屏外緩沖區(qū)(OffScreenBuffer),然后在其中進行渲染,最后將渲染結(jié)果提交到幀緩沖區(qū)內(nèi)(FrameBuffer);這其中還涉及到了兩次上下文的轉(zhuǎn)換,首先把當前上下文轉(zhuǎn)換到屏外緩沖區(qū)(OffScreenBuffer),然后又轉(zhuǎn)換到幀緩沖區(qū)(FrameBuffer)。整個過程會造成很大的消耗。例如蒙板操作:
????在前兩個渲染通道中,GPU分別得到了紋理(texture,也就是那個相機圖標)和layer(藍色的蒙版)的渲染結(jié)果。但這兩個渲染結(jié)果沒有直接放入Render Buffer中,也就表示這是離屏渲染。直到第三個渲染通道,才把兩者組合起來放入Render Buffer中。離屏渲染意味著把渲染結(jié)果臨時保存,等用到時再取出,因此相對于普通渲染更占用資源。
CPU 渲染
????如果我們重寫了drawRect方法,并且使用任何Core Graphics的技術(shù)進行了繪制操作,就涉及到了CPU渲染。由CPU處理的一種特殊渲染方式,在App內(nèi)同步完成,渲染得到的bitmap最后再交由GPU用于顯示,由于CPU自身做渲染的性能也不好,所以這種方式也是需要盡量避免的。
為何需要離屏渲染
????一些復雜的效果,如:圓角,陰影,遮罩,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,無法直接渲染出結(jié)果,所以就需要屏幕外渲染被喚起。
????屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。
????所以當使用離屏渲染的時候會很容易造成性能消耗,因為在OPENGL里離屏渲染會單獨在內(nèi)存中創(chuàng)建一個屏幕外緩沖區(qū)并進行渲染,而屏幕外緩沖區(qū)跟當前屏幕緩沖區(qū)上下文切換是很耗性能的。
觸發(fā)離屏渲染的操作
- shouldRasterize(光柵化)
- masks(遮罩)
- shadows(陰影)
- edge antialiasing(抗鋸齒)
- group opacity(不透明)
- 復雜形狀設(shè)置圓角等
光柵化:
概念:將圖轉(zhuǎn)化為一個個柵格組成的圖象。
特點:每個元素對應幀緩沖區(qū)中的一像素。
????shouldRasterize = YES在其他屬性觸發(fā)離屏渲染的同時,會將光柵化后的內(nèi)容緩存起來,如果對應的layer及其sublayers沒有發(fā)生改變,在下一幀的時候可以直接復用。shouldRasterize = YES,這將隱式的創(chuàng)建一個位圖,各種陰影遮罩等效果也會保存到位圖中并緩存起來,從而減少渲染的頻度(不是矢量圖)。相當于光柵化是把GPU的操作轉(zhuǎn)到CPU上了,生成位圖緩存,直接讀取復用。
????“Color Hits Green and Misses Red”可以檢查當前場景下光柵化操作,綠色表示緩存被復用,紅色表示緩存在被重復創(chuàng)建。
????如果光柵化的層變紅得太頻繁那么光柵化對優(yōu)化可能沒有多少用處。位圖緩存從內(nèi)存中刪除又重新創(chuàng)建得太過頻繁,紅色表明緩存重建得太遲。可以針對性的選擇某個較小而較深的層結(jié)構(gòu)進行光柵化,來嘗試減少渲染時間。
注意:
對于經(jīng)常變動的內(nèi)容,不要開啟光柵化,則會造成大量的離屏渲染,降低圖形性能。
圓角的設(shè)置
-
使用cornerRadius
self.layer.cornerRadius = cornerRadius; self.layer.masksToBounds = YES; //防止子view邊界超過父view //self.clipsToBounds = YES; self.layer.shouldRasterize = YES; //光柵化
- UIView的clipsToBounds與CALayer的maskToBounds的作用一致,防止子view邊界超過父view
- cornerRadius默認情況下只對背景色和border起作用
- 如果最后設(shè)置了 shouldRasterize 為 YES,那也要記住設(shè)置 rasterizationScale 為 contentsScale
-
使用貝塞爾曲線 + maskLayer
- (void)setRoundRect:(CGRect)frame cornerRadius:(CGFloat)cornerRadius { CGRect maskFrame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame)); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:maskFrame cornerRadius:cornerRadius]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; maskLayer.frame = maskFrame; maskLayer.path = path.CGPath; self.layer.mask = maskLayer; }
UIBezierPath對CoreGraphics進行了一層封裝
-
使用CoreGraphics繪制圓角圖片做背景(CPU渲染)
- (void)drawRoundCornerWithCornerRadius:(CGFloat)cornerRadius { CGFloat width = CGRectGetWidth(self.frame); CGFloat height = CGRectGetHeight(self.frame); UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, width, height)]; [self addSubview:imageView]; dispatch_async(dispatch_queue_create("backgroundQueue", DISPATCH_QUEUE_CONCURRENT), ^{ UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, [UIScreen mainScreen].scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextMoveToPoint(context, 0, 0); CGContextAddArcToPoint(context, width, 0, width, height, cornerRadius); CGContextAddArcToPoint(context, width, height, 0, height, cornerRadius); CGContextAddArcToPoint(context, 0, height, 0, 0, cornerRadius); CGContextAddArcToPoint(context, 0, 0, width, 0, cornerRadius); CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); CGContextDrawPath(context, kCGPathStroke); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ imageView.image = image; }); }); }
CoreGraphic通常是線程安全的,所以可以進行異步繪制,顯示的時候再放回主線程
-
將圖片剪切為圓角(針對圖片)
- (UIImage *)setRoundCornerRadius:(CGFloat)cornerRadius { UIImage *image = nil; CGRect imageFrame = CGRectMake(0, 0, self.size.width, self.size.height); UIGraphicsBeginImageContextWithOptions(self.size, NO, [UIScreen mainScreen].scale); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius]; [path addClip]; [self drawInRect:imageFrame]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
使用圓角圖片作為蒙板
如何避免離屏渲染
- 圓角視圖較少,使用使用cornerRadius
- UIImageView 的圓角通過直接截取圖片實現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實現(xiàn)。
- 對于圖形采用異步繪制
- 直接使用圓角素材作為背景
參考資料
iOS離屏渲染優(yōu)化
繪制像素到屏幕上
關(guān)于性能的一些問題(iOS)
解決常見的masksToBounds離屏渲染帶來的性能損耗
iOS 離屏渲染的研究
小心別讓圓角成了你列表的幀數(shù)殺手
iOS 高效添加圓角效果實戰(zhàn)講解
UIKit性能調(diào)優(yōu)實戰(zhàn)講解
iOS 保持界面流暢的技巧
iOS開發(fā):關(guān)于圖形渲染以及界面優(yōu)化的的一些想法
iOS圖形渲染分析