目錄:
- 1.圖像顯示原理
- 2.圖像顯示原理
- 2.1 圖像到屏幕的流程
- 2.2 顯示器顯示的流程
- 3.卡頓、掉幀
- 3.1 垂直同步 Vsync + 雙緩沖機制 Double Buffering
- 2.3 掉幀和屏幕卡頓的本質(zhì)
- 4.離屏渲染
- 4.1 什么事離屏渲染、離屏渲染的過程
- 4.2 既然離屏渲染影響界面,為什么還要用
- 5.觸發(fā)離屏渲染
- 6.如何優(yōu)化
1.引言
先來聊聊為什么要了解離屏渲染?
看看現(xiàn)在app開發(fā)的大環(huán)境,14年的時候在深圳,基本上每個公司都要做一個app。不做一個app你都不一定能拉倒更多的投資。再看看現(xiàn)在,死了一大半,現(xiàn)在的用戶也不想去下載太多的app。一般手機上只留一些常用的,基本全是大廠的app。然后ios這行問的也就越來越難。性能優(yōu)化這個絕對會問,在網(wǎng)上也有許多性能優(yōu)化的總結(jié),但是你不能不知道為什么這么做能優(yōu)化,要知道其為什么。那么,這時候你就需要知道界面是怎么渲染的,什么時候會掉幀,什么時候會卡頓,這些都使得我們非常有必要去了解離屏渲染。
離屏渲染過程
2.圖像顯示原理
2.1 圖像到屏幕的流程
先來看一張圖,我們結(jié)合這張圖來說

首先要明白的一個東西是Render Server 進程,app本身其實并不負責渲染,渲染是有獨立的進程負責的,它就是Render Server 。
當我們在代碼里設置修改了UI界面的時候,其實它本質(zhì)是通過Core Animation修改CALayer。在后續(xù)的核心動畫總結(jié)中 我們會說到UIView和CALayer的關(guān)系,以及核心動畫的設置等等,這個知識點有點多,需要單獨詳細的總結(jié)出來。所以最后按照圖片中的流程顯示。
- 首先,有app處理事件(Handle Events),例如:用戶點擊了一個按鈕,它會觸發(fā)其他的視圖的一個動畫等
- 其次,app通過CPU完成對顯示內(nèi)容的計算 例如:視圖的創(chuàng)建,視圖的布局 圖片文本的繪制等。在完成了對顯示內(nèi)容的計算之后,app對圖層進行打包,并在下一次Runloop時,將其發(fā)送至Render Server
- 上面我們提到,Render Server負責渲染。Render Server通過執(zhí)行Open GL、Core Graphics Metal相關(guān)程序。 調(diào)用GPU
- GPU在物理層完成了對圖像的渲染。
說到這我們就要停一下,我們來看下一個圖

上面的流程圖 細化了GPU到控制器的這一個過程。
GPU 拿到位圖后執(zhí)行頂點著色、圖元裝配、光柵化、片段著色等,最后將渲染的結(jié)果交到了Frame Buffer(幀緩存區(qū)當中)
然后視頻控制器從幀緩存區(qū)中拿到要顯示的對象,顯示到屏幕上
圖片中的黃色虛線暫時不用管,下面在說垂直同步信號的時候,就明白了。
這是從我們代碼中設置UI,然后到屏幕的一個過程。
2.2 顯示器顯示的過程
現(xiàn)在從幀緩存中拿到了渲染的視圖,又該怎么顯示到顯示器上面呢?

從圖中我們也能大致的明白顯示的一個過程。
顯示器的電子束從屏幕的左上方開始逐行顯示,當?shù)谝恍袙呙柰曛蠼又诙?又是從左到右,就這樣一直到屏幕的最下面掃描完成。我們都知道。手機它是有屏幕的刷新次數(shù)的。安卓的現(xiàn)在好多是120的,ios是60。1秒刷新60次,當我們掃描完成以后,屏幕刷新,然后視圖就會顯示出來。
3.UI卡頓 掉幀
3.1垂直同步 Vsync + 雙緩沖機制 Double Buffering
首先我們了解了上面渲染的過程以后,需要考慮遇到一些特別的情況下,該怎么辦?在我們代碼里寫了一個很復雜的UI視圖,然后CPU計算布局、GPU渲染,最后放到緩存區(qū)。如果在電子束開始掃描新的一幀時,位圖還沒有渲染好,而是在掃描到屏幕中間時才渲染完成,被放入幀緩沖器中 。
那么已掃描的部分就是上一幀的畫面,而未掃描的部分就是新一幀的圖像,這樣是不是就造成了屏幕撕裂了。
但是,在我們平常開發(fā)的過程遇到過屏幕撕裂的問題嗎?沒有吧,這是為什么呢?
顯然是蘋果做了優(yōu)化操作了。也就是垂直同步 Vsync + 雙緩沖機制 Double Buffering。
垂直同步 Vsync
垂直同步 Vsync相當于給幀緩存加了鎖,還記得上面說到的那個黃色虛線嘛,在我們掃描完一幀以后,就會發(fā)出一個垂直同步的信號,通知開始掃描下一幀的圖像了。他就像一個位置秩序的,你得給我排隊一個一個來,別插隊。插隊的后果就是屏幕撕裂。
雙緩沖機制 Double Buffering
掃描顯示排隊進行了,這樣在進行下一幀的位圖傳入的時候,也就意味著我要立刻拿到位圖。不能等CPU+GPU計算渲染后再給位圖,這樣就影響性能。要怎么解決這個問題呢?肯定是 在你快要渲染之前你就要把這些都完成了。你就像排隊打針一樣,為了節(jié)省時間肯定事先都會挽起袖子,到醫(yī)生那時,直接一針下去了事。扯遠了 哈哈。想預先渲染好,就需要另外一個緩存來放下一幀的位圖,在它需要掃描的時候,再把渲染好的位圖給了幀緩存,幀緩存拿到以后 開始快樂的掃描 顯示。
一個圖解釋

3.2 掉幀卡頓
垂直同步和雙緩存機制完美的解決了屏幕撕裂的問題,但是又引出一個新的問題:掉幀。
掉幀是什么意思呢?從網(wǎng)上copy了一份圖

其實很好理解,上面我們說了ios的屏幕刷新是60次,那么在一次刷新的過程中,我們CPU+GPU它沒有把新渲染的位圖放到幀緩存區(qū),這時候是不是還是顯示的原來的圖像。當下刷新下一幀的時候,拿到了新的位圖,這里是不是就丟失了一幀。
卡頓的根本原因:
CPU和GPU渲染流水線耗時過長 掉幀
我們平常寫界面的時候,通過一些開源的庫或者自己使用runloop寫的庫來檢測界面卡頓的時候,屏幕刷新率在50以上就很可以了。一般人哪能體驗到掉了10幀。你要刷新率是30,那卡頓想過就很明顯了。
4 離屏渲染
4.1什么是離屏渲染 離屏渲染的過程
是指在GPU在當前屏幕緩沖區(qū)以外開辟一個緩沖區(qū)進行渲染操作.
過程:首先會創(chuàng)建一個當前屏幕緩沖區(qū)以外的新緩存區(qū),屏幕渲染會有一個上下文環(huán)境,離屏渲染的過程就是切花上下文環(huán)境,現(xiàn)充當前屏幕切換到離屏,等結(jié)束以后又將上下文切換回來。所以需要更長的時間來處理。時間一長就可能造成掉幀。
并且 Offscreen Buffer離屏緩存 本身就需要額外的空間,大量的離屏渲染可能造成內(nèi)存過大的壓力。而且離屏緩存區(qū)并不是沒有限制大小的,它是不能超過屏幕總像素的2.5倍。
4.2為什么要使用離屏渲染
1.一些特殊效果需要使用額外的 Offscreen Buffer 來保存渲染的中間狀態(tài),所以不得不使用離屏渲染。
2.處于效率目的,可以將內(nèi)容提前渲染保存在 Offscreen Buffer 中,達到復用的目的。
當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前(下一個VSync信號開始前)不能直接在屏幕中繪制,所以就需要屏幕外渲染。
5.觸發(fā)離屏渲染
- 為圖層設置遮罩(layer.mask)
- 圖層的layer. masksToBounds/view.clipsToBounds屬性設置為true
- 將圖層layer. allowsGroupOpacity設置為yes和layer. opacity<1.0
- 為圖層設置陰影(layer.shadow)
- 為圖層設置shouldRasterize光柵化
6 復雜形狀設置圓角等
7 漸變
8 文本(任何種類,包括UILabel,CATextLayer,Core Text等)
9 使用CGContext在drawRect :方法中繪制大部分情況下會導致離屏渲染,甚至僅僅是一個空的實現(xiàn)。
6 離屏渲染的優(yōu)化
1 圓角優(yōu)化
方法一
iv.layer.cornerRadius = 30;
iv.layer.masksToBounds = YES;
方法二
利用mask設置圓角,利用貝塞斯曲線和CAShapeLayer來完成
CAShapeLayer *mask1 = [[CAShapeLayer alloc] init];
mask1.opacity = 0.5;
mask1.path = [UIBezierPath bezierPathWithOvalInRect:iv.bounds].CGPath;
iv.layer.mask = mask1;
方法三
利用CoreGraphics畫一個圓形上下文,然后把圖片繪制上去
- (void)setCircleImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage * circleImage = [image imageWithCircle];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = circleImage;
});
});
}
#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一張圓形圖片
- (instancetype)imageWithCircle
{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
[path addClip];
[self drawAtPoint:CGPointZero];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
}
shadows(陰影)
設置陰影后,設置CALayer的shadowPath
view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;
mask(遮罩)
不使用mask
使用混合圖層 使用混合圖層,在layer上方疊加相應mask形狀的半透明layer
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
allowsGroupOpacity(組不透明)
關(guān)閉 allowsGroupOpacity 屬性,按產(chǎn)品需求自己控制layer透明度
edge antialiasing(抗鋸齒)
不設置 allowsEdgeAntialiasing 屬性為YES(默認為NO)
當視圖內(nèi)容是靜態(tài)不變時,設置 shouldRasterize(光柵化)為YES,此方案最為實用方便
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;
如果視圖內(nèi)容是動態(tài)變化的,例如cell中的圖片,這個時候使用光柵化會增加系統(tǒng)負荷。