一芯咧、畫面撕裂
1.1畫面撕裂的形成
在介紹離屏渲染
之前我們先了解一下什么是畫面撕裂
,以及其形成的原因:
在游戲中我們有時會遇到這樣的畫面支子,我們很明顯的能看到畫面存在撕裂問題蛮浑,其形成的原因是: GPU
渲染之后會將結(jié)果放在 幀緩存區(qū)
中,視頻控制器
再通過讀取幀緩存區(qū)
中的數(shù)據(jù)進行 數(shù)模轉(zhuǎn)換
來顯示在屏幕上绢慢,顯示的過程是從上至下逐行掃描進行顯示灿渴,如下圖畫面撕裂的形成過程:假設(shè)只有一個幀緩存區(qū)的情況下,幀緩存區(qū)首先放了圖1胰舆,屏幕首先對圖1進行由上至下的掃描骚露,但在掃描到圖2的位置時,GPU
又渲染了一張新的圖片放到了幀緩存區(qū)中(圖3)缚窿,此時屏幕將會繼續(xù)圖2的位置進行掃描幀緩存區(qū)中的圖片棘幸,即是此時的圖3,則屏幕通過由上至下的掃描最終得到的結(jié)果就將是圖4展示的樣子倦零,此時即形成了畫面撕裂
误续。
1.2蘋果解決畫面撕裂的策略
蘋果為應對畫面撕裂問題采取了垂直同步
+雙緩存
的策略。
垂直同步(Vertical synchronization)
:在掃面的過程中加入垂直同步信號
扫茅,確保只有當前幀的圖片掃面完成之后才會繼續(xù)掃面下一幀的圖片蹋嵌。
雙緩存區(qū)
:即采用兩個緩存區(qū)來存儲圖片,屏幕交替掃描兩個緩存區(qū)來進行顯示葫隙。
雖然垂直同步
+雙緩存
的策略解決了畫面撕裂
問題栽烂,但同時也引入了另一個問題:掉幀
。掉幀
最直觀的體現(xiàn)就是屏幕的卡頓,其形成的原因是:當接收到垂直同步信號
的時候愕鼓,CPU
和GPU
還沒有準備好相應的數(shù)據(jù)钙态,即此時幀緩存區(qū)(FrameBuffer)
不存在將要顯示的數(shù)據(jù),視頻控制器拿不到新的數(shù)據(jù)菇晃,就會重復對上一幀的數(shù)據(jù)進行渲染
册倒。
為了應對掉幀
問題,人們又采用的三緩存區(qū)
磺送,但掉幀
歸根結(jié)底的主要原因是CPU
和GPU
處理速度問題驻子,三緩存區(qū)
雖然能在一定程度上抑制掉幀
問題,但并不能從根本上解決估灿。
1.3屏幕卡頓的原因
-
CPU
和GPU
渲染流水線耗時過長崇呵,造成掉幀
; -
垂直同步
+雙緩存
的策略以掉幀
為代價來解決屏幕撕裂問題馅袁; -
三緩存區(qū)
更合理的使用CPU
和GPU
域慷,減少掉幀
的次數(shù),但是并不能從根本上解決掉幀
問題汗销。
二犹褒、離屏渲染
2.1離屏渲染的觸發(fā)
我們一般認為圓角
會觸發(fā)離屏渲染
,但設(shè)置圓角
就一定會觸發(fā)離屏渲染
嗎弛针?首先我們來看一個簡單的demo:
- (void)viewDidLoad {
[super viewDidLoad];
//1.按鈕存在背景圖片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 130, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
[btn1 setImage:[UIImage imageNamed:@"image"] forState:UIControlStateNormal];
btn1.clipsToBounds = YES;
//2.按鈕不存在背景圖片
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 280, 100, 100);
btn2.layer.cornerRadius = 50;
btn2.backgroundColor = [UIColor redColor];
[self.view addSubview:btn2];
btn2.clipsToBounds = YES;
//3.UIImageView 設(shè)置了圖片+背景
UIImageView *imageV1 = [[UIImageView alloc] init];
imageV1.frame = CGRectMake(100, 430, 100, 100);
imageV1.backgroundColor = [UIColor blueColor];
[self.view addSubview:imageV1];
imageV1.layer.cornerRadius = 50;
imageV1.layer.masksToBounds = YES;
imageV1.image = [UIImage imageNamed:@"image"];
//4.UIImageView 只設(shè)置了圖片 無背景色
UIImageView *imageV2 = [[UIImageView alloc] init];
imageV2.frame = CGRectMake(100, 580, 100, 100);
[self.view addSubview:imageV2];
imageV2.layer.cornerRadius = 50;
imageV2.layer.masksToBounds = YES;
imageV2.image = [UIImage imageNamed:@"image"];
}
運行叠骑,并設(shè)置模擬器,Debug
-> Color Off-screen rendered
標記出離屏渲染
的部分:
被標記出黃色的部分是觸發(fā)了離屏渲染
的削茁,而未被標記的則沒有觸發(fā)離屏渲染
宙枷,由此可見設(shè)置了圓角
不一定就會觸發(fā)離屏渲染
,那么觸發(fā)離屏渲染
的條件到底是什么呢茧跋?
2.2離屏渲染的探究
通常情況下的渲染流程是這樣的:APP
通過CPU
和GPU
的合作慰丛,不斷的將渲染的內(nèi)容放到幀緩沖區(qū)(Frame Buffer)
中,屏幕不斷的從幀緩沖區(qū)
中拿到要展示的內(nèi)容瘾杭,實時的顯示在屏幕上璧帝。
離屏渲染
的流程是這樣的:
與普通的渲染不同,離屏渲染
需要創(chuàng)建額外的離屏渲染緩沖區(qū)(offscreen Buffer)
富寿,將渲染好的內(nèi)容放入其中,再等到合適的時機將離屏渲染緩沖區(qū)
中的內(nèi)容進行疊加锣夹、合并页徐,之后再放入幀緩沖區(qū)
中。
從流程圖我們可以看出银萍,離屏渲染
時变勇,需要APP
提前將部分渲染能容保存到離屏渲染緩沖區(qū)
,必要的時候需要對Offscreen Buffer
和Frame Buffer
進行切換,所以勢必需要更多的處理時間搀绣,而且由于離屏渲染
需要開辟額外的空間飞袋,大量的離屏渲染
對勢必也會消耗大量的內(nèi)存。與此同時链患,離屏渲染緩沖區(qū)
也是有大小限制的巧鸭,不能超過屏幕像素點的2.5倍。
大量的離屏渲染
容易造成掉幀
麻捻,所以很多情況下我們能避則避纲仍。但有時我們需要實現(xiàn)一些特殊的效果,需要Offscreen Buffer
保存渲染的中間狀態(tài)時贸毕,我們也不得不使用離屏渲染
郑叠。
以蘋果提供的毛玻璃效果UIBlurEffectView
為例:
整個過程需要經(jīng)歷,渲染內(nèi)容->捕獲內(nèi)容->水平模糊->垂直模糊->合并形成毛玻璃效果明棍,根據(jù)我們對幀緩沖區(qū)
的了解乡革,為節(jié)省空間,幀緩沖區(qū)中
的內(nèi)容繪制到屏幕上之后就會直接移除摊腋,無法做到如此復雜的特效沸版,該過程需要在離屏緩沖區(qū)
進行處理。
有時我們也會為了提高復用效率通過layer的光柵化 shouldRasterize
主動開啟離屏渲染
歌豺,蘋果關(guān)于shouldRasterize
的解釋如下:
When the value of this property is YES , the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
開啟光柵化
后推穷,會觸發(fā)離屏渲染
,Render Serve
r 會強制將 CALayer
的渲染位圖結(jié)果bitmap
保存下來类咧,這樣下次再需要渲染時就可以直接復用馒铃,從而提高效率。
而保存的 bitmap
包含layer
的 subLayer痕惋、圓角区宇、陰影、組透明度 group opacity 等值戳,所以如果layer
的構(gòu)成包含上述幾種元素议谷,結(jié)構(gòu)復雜且需要反復利用,那么就可以考慮打開光柵化
堕虹。
圓角卧晓、陰影、組透明度等會由系統(tǒng)自動觸發(fā)離屏渲染
赴捞,那么打開光柵化
可以節(jié)約第二次及以后的渲染時間逼裆。而多層 subLayer 的情況由于不會自動觸發(fā)離屏渲染
,所以相比之下會多花費第一次離屏渲染
的時間赦政,但是可以節(jié)約后續(xù)的重復渲染的開銷胜宇。
但shouldRasterize
的使用也有一定的限制:
- 如果
layer
不能被復用,則沒有必要打開光柵化
; - 如果
layer
不是靜態(tài)桐愉,需要被頻繁修改财破,比如處于動畫之中,那么開啟離屏渲染
反而影響效率从诲; -
離屏渲染
緩存內(nèi)容有時間限制左痢,緩存內(nèi)容 100ms 內(nèi)如果沒有被使用,那么就會被丟棄盏求,無法進行復用抖锥; -
離屏渲染
緩存空間有限,超過 2.5 倍屏幕像素大小的話也會失效碎罚,無法復用磅废。
layer的構(gòu)成
由上圖我們可以看出layer
由三部分組成,通常我們設(shè)置圓角會設(shè)置layer
的cornerRadius
荆烈,關(guān)于cornerRadius
拯勉,apple
的解釋如下:
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
由上述可知,如果我們只是設(shè)置了cornerRadius
屬性憔购,并不會對content
進行裁剪宫峦,只有我們設(shè)置masksToBounds
才會對內(nèi)容進行裁剪。
圖層的疊加大致遵循“畫家算法”
玫鸟,即由遠及近繪制圖層顯示在屏幕上:
我們可以試想下:如果我們要對一個擁有多個圖層構(gòu)成的視圖進行圓角設(shè)置导绷,如果是存在幀緩沖區(qū)
,那么就會存在一個問題屎飘,每渲染一幀就會丟前面的一幀數(shù)據(jù)妥曲,當我們要設(shè)置圓角時,前面的圖層早已丟失钦购,而離屏緩存區(qū)
不同檐盟,離屏緩存區(qū)
會對渲染的圖層保留一段時間,這段時間就足以我們對多圖層進行押桃、合并葵萎、設(shè)置圓角等操作。想要觸發(fā)離屏渲染
不單單是說設(shè)置了masksToBounds
就會觸發(fā)唱凯,我們更多的要在意的是我們所要操作的圖層羡忘,是否需要保留中間圖層,如果只是單圖層磕昼,肯定不會觸發(fā)離屏渲染
壳坪。
值得注意的是,重寫 drawRect
: 方法并不會觸發(fā)離屏渲染掰烟。重寫 drawRect
:會將 GPU
中的渲染操作轉(zhuǎn)移到 CPU
中完成,并且需要額外開辟內(nèi)存空間。
2.3圓角處理的參考方案
- 方案一:
最簡單的方法就是找UI切帶圓角的圖片纫骑。 - 方案二:
- (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius {
CGFloat w = self.size.width;
CGFloat h = self.size.height;
CGFloat scale = [UIScreen mainScreen].scale;
//防止圓角半徑小于0蝎亚,或者大于寬/高中較小值的一半。
if (cornerRadius < 0) {
cornerRadius = 0;
}else if (cornerRadius > MIN(w, h)/2.0){
cornerRadius = MIN(w, h)/2.0;
}
UIImage *image = nil;
CGRect imageFrame = CGRectMake(0, 0, w, h);
UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
[[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
[self drawInRect:imageFrame];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- 方案三
+ (UIImage *)addMaskToBounds:(CGRect)maskBounds image:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
CGFloat w = maskBounds.size.width;
CGFloat h = maskBounds.size.height;
CGSize size = maskBounds.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGRect imageRect = CGRectMake(0, 0, w, h);
if (cornerRadius < 0) {
cornerRadius = 0;
}else if (cornerRadius > MIN(w, h)/2.0){
cornerRadius = MIN(w, h)/2.0;
}
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
[[UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:cornerRadius] addClip];
[image drawInRect:imageRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- 方案四
@interface RoundImageView()
@property (nonatomic, strong) UIImageView *maskImageView;
@end
@implementation RoundImageView
- (instancetype)init {
self = [super init];
if (self) {
_maskImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_maskImageView.image = [UIImage imageNamed:@"ic_imageView_mask"];//加圓角圖片蓋在上面
[self addSubview:_maskImageView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect bounds = self.bounds;
_maskImageView.frame = bounds;
}
另附:YYImage的圓角處理
2.4常見觸發(fā)離屏渲染的幾種情況
- 使用了
mask
的layer
(layer.mask
) - 需要進行裁剪的
layer
(layer.masksToBounds / view.clipsToBounds
) - 設(shè)置了組透明度為Yes先馆,并且透明度不為1的
layer
(layer.allowsGroupOpacity / layer.opacity
) - 添加了投影的
layer
(layer.shadow
) - 采用了
光柵化
的layer
(layer.shouldRasterize
) - 繪制了文字的
layer
(UILabel
,CATextLayer
,CoreText
等)