Hello OpenGL--003:離屏渲染

一芯咧、畫面撕裂

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ū)來進行顯示葫隙。

蘋果官方關(guān)于雙緩存區(qū)示意圖

雖然垂直同步+雙緩存的策略解決了畫面撕裂問題栽烂,但同時也引入了另一個問題:掉幀掉幀最直觀的體現(xiàn)就是屏幕的卡頓,其形成的原因是:當接收到垂直同步信號的時候愕鼓,CPUGPU還沒有準備好相應的數(shù)據(jù)钙态,即此時幀緩存區(qū)(FrameBuffer)不存在將要顯示的數(shù)據(jù),視頻控制器拿不到新的數(shù)據(jù)菇晃,就會重復對上一幀的數(shù)據(jù)進行渲染册倒。

掉幀形成的示意圖

為了應對掉幀問題,人們又采用的三緩存區(qū)磺送,但掉幀歸根結(jié)底的主要原因是CPUGPU處理速度問題驻子,三緩存區(qū)雖然能在一定程度上抑制掉幀問題,但并不能從根本上解決估灿。

1.3屏幕卡頓的原因
  • CPUGPU渲染流水線耗時過長崇呵,造成掉幀
  • 垂直同步+雙緩存的策略以掉幀為代價來解決屏幕撕裂問題馅袁;
  • 三緩存區(qū)更合理的使用CPUGPU域慷,減少掉幀的次數(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 標記出離屏渲染的部分:

離屏渲染提示

這樣我們就會得到如下結(jié)果:
運行結(jié)果

被標記出黃色的部分是觸發(fā)了離屏渲染的削茁,而未被標記的則沒有觸發(fā)離屏渲染宙枷,由此可見設(shè)置了圓角不一定就會觸發(fā)離屏渲染,那么觸發(fā)離屏渲染的條件到底是什么呢茧跋?

2.2離屏渲染的探究

通常情況下的渲染流程是這樣的:
APP渲染流程

APP通過CPUGPU的合作慰丛,不斷的將渲染的內(nèi)容放到幀緩沖區(qū)(Frame Buffer)中,屏幕不斷的從幀緩沖區(qū)中拿到要展示的內(nèi)容瘾杭,實時的顯示在屏幕上璧帝。

離屏渲染的流程是這樣的:

離屏渲染流程

與普通的渲染不同,離屏渲染需要創(chuàng)建額外的離屏渲染緩沖區(qū)(offscreen Buffer)富寿,將渲染好的內(nèi)容放入其中,再等到合適的時機將離屏渲染緩沖區(qū)中的內(nèi)容進行疊加锣夹、合并页徐,之后再放入幀緩沖區(qū)中。
從流程圖我們可以看出银萍,離屏渲染時变勇,需要APP提前將部分渲染能容保存到離屏渲染緩沖區(qū),必要的時候需要對Offscreen BufferFrame Buffer進行切換,所以勢必需要更多的處理時間搀绣,而且由于離屏渲染需要開辟額外的空間飞袋,大量的離屏渲染對勢必也會消耗大量的內(nèi)存。與此同時链患,離屏渲染緩沖區(qū)也是有大小限制的巧鸭,不能超過屏幕像素點的2.5倍。
大量的離屏渲染容易造成掉幀麻捻,所以很多情況下我們能避則避纲仍。但有時我們需要實現(xiàn)一些特殊的效果,需要Offscreen Buffer保存渲染的中間狀態(tài)時贸毕,我們也不得不使用離屏渲染郑叠。
以蘋果提供的毛玻璃效果UIBlurEffectView為例:

UIVisualEffectView with UIBlurEffect Rendering passes

整個過程需要經(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 Server 會強制將 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è)置layercornerRadius荆烈,關(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的圓角處理


YYImage的圓角處理
2.4常見觸發(fā)離屏渲染的幾種情況
  • 使用了masklayerlayer.mask
  • 需要進行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
  • 設(shè)置了組透明度為Yes先馆,并且透明度不為1的layer(layer.allowsGroupOpacity / layer.opacity)
  • 添加了投影的layer (layer.shadow)
  • 采用了光柵化layerlayer.shouldRasterize
  • 繪制了文字的layerUILabel, CATextLayer, CoreText等)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末发框,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子煤墙,更是在濱河造成了極大的恐慌梅惯,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仿野,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機域仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門匈辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人球涛,你說我怎么就攤上這事劣针。” “怎么了亿扁?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵捺典,是天一觀的道長。 經(jīng)常有香客問我从祝,道長襟己,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任哄褒,我火速辦了婚禮稀蟋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呐赡。我一直安慰自己退客,他們只是感情好,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布链嘀。 她就那樣靜靜地躺著萌狂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怀泊。 梳的紋絲不亂的頭發(fā)上茫藏,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音霹琼,去河邊找鬼务傲。 笑死凉当,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的售葡。 我是一名探鬼主播看杭,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挟伙!你這毒婦竟也來了楼雹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤尖阔,失蹤者是張志新(化名)和其女友劉穎贮缅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體介却,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡谴供,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了筷笨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憔鬼。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖胃夏,靈堂內(nèi)的尸體忽然破棺而出轴或,到底是詐尸還是另有隱情,我是刑警寧澤仰禀,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布照雁,位于F島的核電站,受9級特大地震影響答恶,放射性物質(zhì)發(fā)生泄漏饺蚊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一悬嗓、第九天 我趴在偏房一處隱蔽的房頂上張望污呼。 院中可真熱鬧,春花似錦包竹、人聲如沸燕酷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苗缩。三九已至,卻和暖如春声诸,著一層夾襖步出監(jiān)牢的瞬間酱讶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工彼乌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泻肯,地道東北人渊迁。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像灶挟,于是被迫代替她去往敵國和親宫纬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354