本文參考:https://imlifengfeng.github.io/article/593/
一、概述
OpenGL ES是一套多功能開放標(biāo)準(zhǔn)的用于嵌入系統(tǒng)的C-based的圖形庫,用于2D和3D數(shù)據(jù)的可視化。OpenGL被設(shè)計(jì)用來轉(zhuǎn)換一組圖形調(diào)用功能到底層圖形硬件(GPU),由GPU執(zhí)行圖形命令,用來實(shí)現(xiàn)復(fù)雜的圖形操作和運(yùn)算,從而能夠高性能、高幀率利用GPU提供的2D和3D繪制能力。iOS系統(tǒng)默認(rèn)支持OpenGl ES1.0、ES2.0以及ES3.0 3個(gè)版本,三者之間并不是簡單的版本升級,設(shè)計(jì)理念甚至完全不同。GPU屏幕渲染方式中有一種方式為離屏渲染,處理不好離屏渲染往往會對APP的性能產(chǎn)生較大的影響。
二、當(dāng)前屏幕渲染與離屏渲染
OpenGL中,GPU屏幕渲染有兩種方式:
(1)On-Screen Rendering (當(dāng)前屏幕渲染)
指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行。
(2)Off-Screen Rendering (離屏渲染)
指的是在GPU在當(dāng)前屏幕緩沖區(qū)以外開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
當(dāng)前屏幕渲染不需要額外創(chuàng)建新的緩存,也不需要開啟新的上下文,相對于離屏渲染性能更好。但是受當(dāng)前屏幕渲染的局限因素限制(只有自身上下文、屏幕緩存有限等),當(dāng)前屏幕渲染有些情況下的渲染解決不了的,就使用到離屏渲染。
相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的,主要體現(xiàn)在兩個(gè)方面:
(1)創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染,首先要創(chuàng)建一個(gè)新的緩沖區(qū)。
(2)上下文切換
離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。而上下文環(huán)境的切換是要付出很大代價(jià)的。
特殊的“離屏渲染”:CPU渲染
如果我們重寫了drawRect方法,并且使用任何Core Graphics的技術(shù)進(jìn)行了繪制操作,就涉及到了CPU渲染。整個(gè)渲染過程由CPU在App內(nèi)同步地完成,渲染得到的bitmap(位圖)最后再交由GPU用于顯示。
Designing for iOS: Graphics & Performance 這篇文章也提到了使用 Core Graphics API 會觸發(fā)離屏渲染。 蘋果 iOS 4.1-8 時(shí)期的 UIKit 組成員Andy Matuschak也曾對這個(gè)說法進(jìn)行解釋:「Core Graphics 的繪制 API 的確會觸發(fā)離屏渲染,但不是那種 GPU 的離屏渲染。使用 Core Graphics 繪制 API 是在 CPU 上執(zhí)行,觸發(fā)的是 CPU 版本的離屏渲染。」
三、為什么要有離屏渲染
大家高中物理應(yīng)該學(xué)過顯示器是如何顯示圖像的:需要顯示的圖像經(jīng)過CRT電子槍以極快的速度一行一行的掃描,掃描出來就呈現(xiàn)了一幀畫面,隨后電子槍又會回到初始位置循環(huán)掃描,形成了我們看到的圖片或視頻。
為了讓顯示器的顯示跟視頻控制器同步,當(dāng)電子槍新掃描一行的時(shí)候,準(zhǔn)備掃描的時(shí)發(fā)送一個(gè)水平同步信號(HSync信號),顯示器的刷新頻率就是HSync信號產(chǎn)生的頻率。然后CPU計(jì)算好frame等屬性,將計(jì)算好的內(nèi)容交給GPU去渲染,GPU渲染好之后就會放入幀緩沖區(qū)。然后視頻控制器會按照HSync信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器,就顯示出來了。具體的大家自行查找資料或詢問相關(guān)專業(yè)人士,這里只參考網(wǎng)上資料做一個(gè)簡單的描述。
離屏渲染的代價(jià)很高,想要進(jìn)行離屏渲染,首選要創(chuàng)建一個(gè)新的緩沖區(qū),屏幕渲染會有一個(gè)上下文環(huán)境的一個(gè)概念,離屏渲染的整個(gè)過程需要切換上下文環(huán)境,先從當(dāng)前屏幕切換到離屏,等結(jié)束后,又要將上下文環(huán)境切換回來。這也是為什么會消耗性能的原因了。
由于垂直同步的機(jī)制,如果在一個(gè) HSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機(jī)會再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。
既然離屏渲染這么耗性能,為什么有這套機(jī)制呢?
有些效果被認(rèn)為不能直接呈現(xiàn)于屏幕,而需要在別的地方做額外的處理預(yù)合成。圖層屬性的混合體沒有預(yù)合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個(gè)屏幕外上下文中被渲染(不論CPU還是GPU)。
下面的情況或操作會引發(fā)離屏渲染:
為圖層設(shè)置遮罩(layer.mask)
將圖層的layer.masksToBounds / view.clipsToBounds屬性設(shè)置為true
將圖層layer.allowsGroupOpacity屬性設(shè)置為YES和layer.opacity小于1.0
為圖層設(shè)置陰影(layer.shadow *)。
為圖層設(shè)置layer.shouldRasterize=true
具有l(wèi)ayer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的圖層
文本(任何種類,包括UILabel,CATextLayer,Core Text等)。
使用CGContext在drawRect :方法中繪制大部分情況下會導(dǎo)致離屏渲染,甚至僅僅是一個(gè)空的實(shí)現(xiàn)。
四、優(yōu)化方案
官方對離屏渲染產(chǎn)生性能問題也進(jìn)行了優(yōu)化:
iOS 9.0 之前UIimageView跟UIButton設(shè)置圓角都會觸發(fā)離屏渲染。
iOS 9.0 之后UIButton設(shè)置圓角會觸發(fā)離屏渲染,而UIImageView里png圖片設(shè)置圓角不會觸發(fā)離屏渲染了,如果設(shè)置其他陰影效果之類的還是會觸發(fā)離屏渲染的。
1、圓角優(yōu)化
在APP開發(fā)中,圓角圖片還是經(jīng)常出現(xiàn)的。如果一個(gè)界面中只有少量圓角圖片或許對性能沒有非常大的影響,但是當(dāng)圓角圖片比較多的時(shí)候就會APP性能產(chǎn)生明顯的影響。
我們設(shè)置圓角一般通過如下方式:
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
這樣處理的渲染機(jī)制是GPU在當(dāng)前屏幕緩沖區(qū)外新開辟一個(gè)渲染緩沖區(qū)進(jìn)行工作,也就是離屏渲染,這會給我們帶來額外的性能損耗,如果這樣的圓角操作達(dá)到一定數(shù)量,會觸發(fā)緩沖區(qū)的頻繁合并和上下文的的頻繁切換,性能的代價(jià)會宏觀地表現(xiàn)在用戶體驗(yàn)上——掉幀。
優(yōu)化方案1:使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個(gè)圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//開始對imageView進(jìn)行畫圖
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用貝塞爾曲線畫出一個(gè)圓形圖
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//結(jié)束畫圖
UIGraphicsEndImageContext();
優(yōu)化方案2:使用CAShapeLayer和UIBezierPath設(shè)置圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//設(shè)置大小
maskLayer.frame = imageView.bounds;
//設(shè)置圖形樣子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
對于方案2需要解釋的是:
- CAShapeLayer繼承于CALayer,可以使用CALayer的所有屬性值;
- CAShapeLayer需要貝塞爾曲線配合使用才有意義(也就是說才有效果)
- 使用CAShapeLayer(屬于CoreAnimation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect(繼承于CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形
- CAShapeLayer動畫渲染直接提交到手機(jī)的GPU當(dāng)中,相較于view的drawRect方法使用CPU渲染而言,其效率極高,能大大優(yōu)化內(nèi)存使用情況。
總的來說就是用CAShapeLayer的內(nèi)存消耗少,渲染速度快,建議使用優(yōu)化方案2。
2、shadow優(yōu)化
對于shadow,如果圖層是個(gè)簡單的幾何圖形或者圓角圖形,我們可以通過設(shè)置shadowPath來優(yōu)化性能,能大幅提高性能。示例如下:
imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
我們還可以通過設(shè)置shouldRasterize屬性值為YES來強(qiáng)制開啟離屏渲染。其實(shí)就是光柵化(Rasterization)。既然離屏渲染這么不好,為什么我們還要強(qiáng)制開啟呢?當(dāng)一個(gè)圖像混合了多個(gè)圖層,每次移動時(shí),每一幀都要重新合成這些圖層,十分消耗性能。當(dāng)我們開啟光柵化后,會在首次產(chǎn)生一個(gè)位圖緩存,當(dāng)再次使用時(shí)候就會復(fù)用這個(gè)緩存。但是如果圖層發(fā)生改變的時(shí)候就會重新產(chǎn)生位圖緩存。所以這個(gè)功能一般不能用于UITableViewCell中,cell的復(fù)用反而降低了性能。最好用于圖層較多的靜態(tài)內(nèi)容的圖形。而且產(chǎn)生的位圖緩存的大小是有限制的,一般是2.5個(gè)屏幕尺寸。在100ms之內(nèi)不使用這個(gè)緩存,緩存也會被刪除。所以我們要根據(jù)使用場景而定。
3、其他的一些優(yōu)化建議
- 當(dāng)我們需要圓角效果時(shí),可以使用一張中間透明圖片蒙上去
- 使用ShadowPath指定layer陰影效果路徑
- 使用異步進(jìn)行l(wèi)ayer渲染(Facebook開源的異步繪制框架AsyncDisplayKit)
- 設(shè)置layer的opaque值為YES,減少復(fù)雜圖層合成
- 盡量使用不包含透明(alpha)通道的圖片資源
- 盡量設(shè)置layer的大小值為整形值
- 直接讓美工把圖片切成圓角進(jìn)行顯示,這是效率最高的一種方案
- 很多情況下用戶上傳圖片進(jìn)行顯示,可以讓服務(wù)端處理圓角
- 使用代碼手動生成圓角Image設(shè)置到要顯示的View上,利用UIBezierPath(CoreGraphics框架)畫出來圓角圖片