GPU 渲染機(jī)制:CPU計(jì)算好顯示內(nèi)容提交到GPU存谎,GPU渲染完成后將渲染結(jié)果放入幀緩沖區(qū)frame buffer,隨后視頻控制器會(huì)按照VSync信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù)鳞芙,經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示猿诸。
GPU 屏幕渲染有以下兩種方式:
● 1)On-Screen Rendering,意為當(dāng)前屏幕渲染子姜,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行。
● 2)Off-Screen Rendering楼入,意為離屏渲染哥捕,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行渲染操作牧抽。
特殊的離屏渲染:如果將不在GPU的當(dāng)前屏幕緩沖區(qū)中進(jìn)行的渲染都稱為離屏渲染,那么就還有另一種特殊的“離屏渲染”方式:CPU渲染扭弧。如果我們重寫了drawRect方法阎姥,并且使用任何Core Graphics的技術(shù)進(jìn)行了繪制操作记舆,就涉及到了CPU渲染鸽捻。整個(gè)渲染過(guò)程由CPU在App內(nèi)同步地完成,渲染得到的bitmap最后再交由GPU用于顯示泽腮。備注:Core Graphics通常是線程安全的御蒲,所以可以進(jìn)行異步繪制,顯示的時(shí)候再放回主線程诊赊,一個(gè)簡(jiǎn)單的異步繪制過(guò)程大致如下:
(void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
離屏渲染的觸發(fā)方式:
1)shouldRasterize(光柵化)厚满,光柵化是比較特別的一種。光柵化概念:將圖轉(zhuǎn)化為一個(gè)個(gè)柵格組成的圖象碧磅。光柵化特點(diǎn):每個(gè)元素對(duì)應(yīng)幀緩沖區(qū)中的一像素碘箍。shouldRasterize = YES 在其他屬性觸發(fā)離屏渲染的同時(shí),會(huì)將光柵化后的內(nèi)容緩存起來(lái)鲸郊,如果對(duì)應(yīng)的 layer 及其 sublayers 沒(méi)有發(fā)生改變,在下一幀的時(shí)候可以直接復(fù)用。shouldRasterize = YES 這將隱式的創(chuàng)建一個(gè)位圖举瑰,各種陰影遮罩等效果也會(huì)保存到位圖中并緩存起來(lái)船侧,從而減少渲染的頻度。相當(dāng)于光柵化是把 GPU 的操作轉(zhuǎn)到 CPU 上了职辨,生成位圖緩存盗蟆,直接讀取復(fù)用。當(dāng)你使用光柵化時(shí)舒裤,你可以開(kāi)啟 Color Hits Green and Misses Red 來(lái)檢查該場(chǎng)景下光柵化操作是否是一個(gè)好的選擇喳资。綠色表示緩存被復(fù)用,紅色表示緩存在被重復(fù)創(chuàng)建腾供。如果光柵化的層變紅得太頻繁那么光柵化對(duì)優(yōu)化可能沒(méi)有多少用處仆邓。位圖緩存從內(nèi)存中刪除又重新創(chuàng)建得太過(guò)頻繁,紅色表明緩存重建得太遲台腥『曜福可以針對(duì)性的選擇某個(gè)較小而較深的層結(jié)構(gòu)進(jìn)行光柵化,來(lái)嘗試減少渲染時(shí)間黎侈。對(duì)于經(jīng)常變動(dòng)的內(nèi)容察署,這個(gè)時(shí)候不要開(kāi)啟,否則會(huì)造成性能的浪費(fèi)峻汉。例如經(jīng)常打交道的 TableViewCell贴汪,因?yàn)?TableViewCell 的重繪是很頻繁的(因?yàn)?Cell 的復(fù)用)脐往,如果Cell的內(nèi)容不斷變化,則Cell需要不斷重繪扳埂,如果此時(shí)設(shè)置了 cell.layer 可光柵化业簿,則會(huì)造成大量的離屏渲染,降低圖形性能阳懂。
2)masks(遮罩)
3)shadows(陰影)
4)edge antialiasing(抗鋸齒)
5)group opacity(不透明)
6)復(fù)雜形狀設(shè)置圓角等
7)漸變
為什么會(huì)使用離屏渲染:當(dāng)使用圓角梅尤,陰影,遮罩的時(shí)候岩调,圖層屬性的混合體被指定為在未預(yù)合成之前(下一個(gè) VSync 信號(hào)開(kāi)始前)不能直接在屏幕中繪制巷燥,所以就需要屏幕外渲染被喚起。屏幕外渲染并不意味著軟件繪制号枕,但是它意味著圖層必須在被顯示之前在一個(gè)屏幕外上下文中被渲染(不論 CPU 還是 GPU)缰揪。所以當(dāng)使用離屏渲染的時(shí)候會(huì)很容易造成性能消耗,因?yàn)殡x屏渲染會(huì)單獨(dú)在內(nèi)存中創(chuàng)建一個(gè)屏幕外緩沖區(qū)并進(jìn)行渲染葱淳,而屏幕外緩沖區(qū)跟當(dāng)前屏幕緩沖區(qū)上下文切換是很耗性能的钝腺。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi)赞厕,CPU或者GPU沒(méi)有完成內(nèi)容提交艳狐,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示坑傅,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變僵驰。這就是界面卡頓的原因。
Instruments 監(jiān)測(cè)離屏渲染:
1)Color Offscreen-Rendered Yellow唁毒,開(kāi)啟后會(huì)把那些需要離屏渲染的圖層高亮成黃色蒜茴,這就意味著黃色圖層可能存在性能問(wèn)題。
2)Color Hits Green and Misses Red浆西,如果 shouldRasterize 被設(shè)置成YES粉私,對(duì)應(yīng)的渲染結(jié)果會(huì)被緩存,如果圖層是綠色近零,就表示這些緩存被復(fù)用诺核;如果是紅色就表示緩存會(huì)被重復(fù)創(chuàng)建,這就表示該處存在性能問(wèn)題了久信。
GPU離屏渲染的性能影響
GPU的操作是高度流水線化的窖杀。本來(lái)所有計(jì)算工作都在有條不紊地正在向frame buffer輸出,此時(shí)突然收到指令裙士,需要輸出到另一塊內(nèi)存入客,那么流水線中正在進(jìn)行的一切都不得不被丟棄,切換到只能服務(wù)于我們當(dāng)前的“切圓角”操作。等到完成以后再次清空桌硫,再回到向frame buffer輸出的正常流程夭咬。
在tableView或者collectionView中,滾動(dòng)的每一幀變化都會(huì)觸發(fā)每個(gè)cell的重新繪制铆隘,因此一旦存在離屏渲染卓舵,上面提到的上下文切換就會(huì)每秒發(fā)生60次,并且很可能每一幀有幾十張的圖片要求這么做膀钠,對(duì)于GPU的性能沖擊可想而知(GPU非常擅長(zhǎng)大規(guī)模并行計(jì)算掏湾,但是我想頻繁的上下文切換顯然不在其設(shè)計(jì)考量之中)
相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的托修,主要體現(xiàn)在兩個(gè)方面:
(1)創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染忘巧,首先要?jiǎng)?chuàng)建一個(gè)新的緩沖區(qū)。
(2)上下文切換
離屏渲染的整個(gè)過(guò)程睦刃,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結(jié)束以后十酣,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕涩拙。而上下文環(huán)境的切換是要付出很大代價(jià)的。
善用離屏渲染
盡管離屏渲染開(kāi)銷很大耸采,但是當(dāng)我們無(wú)法避免它的時(shí)候兴泥,可以想辦法把性能影響降到最低。優(yōu)化思路也很簡(jiǎn)單:既然已經(jīng)花了不少精力把圖片裁出了圓角虾宇,如果我能把結(jié)果緩存下來(lái)搓彻,那么下一幀渲染就可以復(fù)用這個(gè)成果,不需要再重新畫一遍了嘱朽。
CALayer為這個(gè)方案提供了對(duì)應(yīng)的解法:shouldRasterize旭贬。一旦被設(shè)置為true,Render Server就會(huì)強(qiáng)制把layer的渲染結(jié)果(包括其子layer搪泳,以及圓角稀轨、陰影、group opacity等等)保存在一塊內(nèi)存中岸军,這樣一來(lái)在下一幀仍然可以被復(fù)用奋刽,而不會(huì)再次觸發(fā)離屏渲染。有幾個(gè)需要注意的點(diǎn):
● shouldRasterize的主旨在于降低性能損失艰赞,但總是至少會(huì)觸發(fā)一次離屏渲染佣谐。如果你的layer本來(lái)并不復(fù)雜,也沒(méi)有圓角陰影等等方妖,打開(kāi)這個(gè)開(kāi)關(guān)反而會(huì)增加一次不必要的離屏渲染
● 離屏渲染緩存有空間上限狭魂,最多不超過(guò)屏幕總像素的2.5倍大小
● 一旦緩存超過(guò)100ms沒(méi)有被使用,會(huì)自動(dòng)被丟棄
● layer的內(nèi)容(包括子layer)必須是靜態(tài)的,因?yàn)橐坏┌l(fā)生變化(如resize趁蕊,動(dòng)畫)坞生,之前辛苦處理得到的緩存就失效了。如果這件事頻繁發(fā)生掷伙,我們就又回到了“每一幀都需要離屏渲染”的情景是己,而這正是開(kāi)發(fā)者需要極力避免的。針對(duì)這種情況任柜,Xcode提供了“Color Hits Green and Misses Red”的選項(xiàng)卒废,幫助我們查看緩存的使用是否符合預(yù)期
● 其實(shí)除了解決多次離屏渲染的開(kāi)銷,shouldRasterize在另一個(gè)場(chǎng)景中也可以使用:如果layer的子結(jié)構(gòu)非常復(fù)雜宙地,渲染一次所需時(shí)間較長(zhǎng)摔认,同樣可以打開(kāi)這個(gè)開(kāi)關(guān),把layer繪制到一塊緩存宅粥,然后在接下來(lái)復(fù)用這個(gè)結(jié)果参袱,這樣就不需要每次都重新繪制整個(gè)layer樹(shù)了
什么時(shí)候需要CPU渲染
渲染性能的調(diào)優(yōu),其實(shí)始終是在做一件事:平衡CPU和GPU的負(fù)載秽梅,讓他們盡量做各自最擅長(zhǎng)的工作抹蚀。
平衡CPU和GPU的負(fù)載
絕大多數(shù)情況下,得益于GPU針對(duì)圖形處理的優(yōu)化企垦,我們都會(huì)傾向于讓GPU來(lái)完成渲染任務(wù)环壤,而給CPU留出足夠時(shí)間處理各種各樣復(fù)雜的App邏輯。為此Core Animation做了大量的工作钞诡,盡量把渲染工作轉(zhuǎn)換成適合GPU處理的形式(也就是所謂的硬件加速郑现,如layer composition,設(shè)置backgroundColor等等)荧降。
但是對(duì)于一些情況接箫,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,由于GPU并不擅長(zhǎng)做這些工作誊抛,不得不先由CPU來(lái)處理好以后列牺,再把結(jié)果作為texture傳給GPU。除此以外拗窃,有時(shí)候也會(huì)遇到GPU實(shí)在忙不過(guò)來(lái)的情況瞎领,而CPU相對(duì)空閑(GPU瓶頸),這時(shí)可以讓CPU分擔(dān)一部分工作随夸,提高整體效率九默。
來(lái)自WWDC18 session 221,可以看到Core Text基于Core Graphics
一個(gè)典型的例子是宾毒,我們經(jīng)常會(huì)使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)驼修。整個(gè)過(guò)程全部是由CPU完成的。這樣一來(lái)既然我們已經(jīng)得到了想要的效果,就不需要再另外給圖片容器設(shè)置cornerRadius乙各。另一個(gè)好處是墨礁,我們可以靈活地控制裁剪和緩存的時(shí)機(jī),巧妙避開(kāi)CPU和GPU最繁忙的時(shí)段耳峦,達(dá)到平滑性能波動(dòng)的目的恩静。
這里有幾個(gè)需要注意的點(diǎn):
● 渲染不是CPU的強(qiáng)項(xiàng),調(diào)用CoreGraphics會(huì)消耗其相當(dāng)一部分計(jì)算時(shí)間蹲坷,并且我們也不愿意因此阻塞用戶操作驶乾,因此一般來(lái)說(shuō)CPU渲染都在后臺(tái)線程完成(這也是AsyncDisplayKit的主要思想),然后再回到主線程上循签,把渲染結(jié)果傳回CoreAnimation级乐。這樣一來(lái),多線程間數(shù)據(jù)同步會(huì)增加一定的復(fù)雜度
● 同樣因?yàn)镃PU渲染速度不夠快县匠,因此只適合渲染靜態(tài)的元素风科,如文字、圖片(想象一下沒(méi)有硬件加速的視頻解碼聚唐,性能慘不忍睹)
● 作為渲染結(jié)果的bitmap數(shù)據(jù)量較大(形式上一般為解碼后的UIImage)丐重,消耗內(nèi)存較多,所以應(yīng)該在使用完及時(shí)釋放杆查,并在需要的時(shí)候重新生成,否則很容易導(dǎo)致OOM
● 如果你選擇使用CPU來(lái)做渲染臀蛛,那么就沒(méi)有理由再觸發(fā)GPU的離屏渲染了亲桦,否則會(huì)同時(shí)存在兩塊內(nèi)容相同的內(nèi)存,而且CPU和GPU都會(huì)比較辛苦
● 一定要使用Instruments的不同工具來(lái)測(cè)試性能浊仆,而不是僅憑猜測(cè)來(lái)做決定.
優(yōu)化方案
官方對(duì)離屏渲染產(chǎn)生性能問(wèn)題也進(jìn)行了優(yōu)化:
iOS 9.0 之前UIimageView跟UIButton設(shè)置圓角都會(huì)觸發(fā)離屏渲染客峭。
iOS 9.0 之后UIButton設(shè)置圓角會(huì)觸發(fā)離屏渲染,而UIImageView里png圖片設(shè)置圓角不會(huì)觸發(fā)離屏渲染了抡柿,如果設(shè)置其他陰影效果之類的還是會(huì)觸發(fā)離屏渲染的舔琅。
1、圓角優(yōu)化
在APP開(kāi)發(fā)中洲劣,圓角圖片還是經(jīng)常出現(xiàn)的备蚓。如果一個(gè)界面中只有少量圓角圖片或許對(duì)性能沒(méi)有非常大的影響,但是當(dāng)圓角圖片比較多的時(shí)候就會(huì)APP性能產(chǎn)生明顯的影響囱稽。
我們?cè)O(shè)置圓角一般通過(guò)如下方式:
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
這樣處理的渲染機(jī)制是GPU在當(dāng)前屏幕緩沖區(qū)外新開(kāi)辟一個(gè)渲染緩沖區(qū)進(jìn)行工作郊尝,也就是離屏渲染,這會(huì)給我們帶來(lái)額外的性能損耗战惊,如果這樣的圓角操作達(dá)到一定數(shù)量流昏,會(huì)觸發(fā)緩沖區(qū)的頻繁合并和上下文的的頻繁切換,性能的代價(jià)會(huì)宏觀地表現(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"];
//開(kāi)始對(duì)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;
對(duì)于方案2需要解釋的是:
● CAShapeLayer繼承于CALayer,可以使用CALayer的所有屬性值况凉;
● CAShapeLayer需要貝塞爾曲線配合使用才有意義(也就是說(shuō)才有效果)
● 使用CAShapeLayer(屬于CoreAnimation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect(繼承于CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形
● CAShapeLayer動(dòng)畫渲染直接提交到手機(jī)的GPU當(dāng)中谚鄙,相較于view的drawRect方法使用CPU渲染而言,其效率極高刁绒,能大大優(yōu)化內(nèi)存使用情況闷营。
總的來(lái)說(shuō)就是用CAShapeLayer的內(nèi)存消耗少,渲染速度快膛锭,建議使用優(yōu)化方案2粮坞。
2、shadow優(yōu)化
對(duì)于shadow初狰,如果圖層是個(gè)簡(jiǎn)單的幾何圖形或者圓角圖形莫杈,我們可以通過(guò)設(shè)置shadowPath來(lái)優(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;
我們還可以通過(guò)設(shè)置shouldRasterize屬性值為YES來(lái)強(qiáng)制開(kāi)啟離屏渲染筝闹。其實(shí)就是光柵化(Rasterization)。既然離屏渲染這么不好腥光,為什么我們還要強(qiáng)制開(kāi)啟呢关顷?當(dāng)一個(gè)圖像混合了多個(gè)圖層,每次移動(dòng)時(shí)武福,每一幀都要重新合成這些圖層议双,十分消耗性能。當(dāng)我們開(kāi)啟光柵化后捉片,會(huì)在首次產(chǎn)生一個(gè)位圖緩存平痰,當(dāng)再次使用時(shí)候就會(huì)復(fù)用這個(gè)緩存。但是如果圖層發(fā)生改變的時(shí)候就會(huì)重新產(chǎn)生位圖緩存伍纫。所以這個(gè)功能一般不能用于UITableViewCell中宗雇,cell的復(fù)用反而降低了性能。最好用于圖層較多的靜態(tài)內(nèi)容的圖形莹规。而且產(chǎn)生的位圖緩存的大小是有限制的赔蒲,一般是2.5個(gè)屏幕尺寸。在100ms之內(nèi)不使用這個(gè)緩存良漱,緩存也會(huì)被刪除舞虱。所以我們要根據(jù)使用場(chǎng)景而定。
3债热、其他的一些優(yōu)化建議
● 當(dāng)我們需要圓角效果時(shí)砾嫉,可以使用一張中間透明圖片蒙上去
● 使用ShadowPath指定layer陰影效果路徑
● 使用異步進(jìn)行l(wèi)ayer渲染(Facebook開(kāi)源的異步繪制框架AsyncDisplayKit)
● 設(shè)置layer的opaque值為YES,減少?gòu)?fù)雜圖層合成
● 盡量使用不包含透明(alpha)通道的圖片資源
● 盡量設(shè)置layer的大小值為整形值
● 直接讓美工把圖片切成圓角進(jìn)行顯示窒篱,這是效率最高的一種方案
● 很多情況下用戶上傳圖片進(jìn)行顯示焕刮,可以讓服務(wù)端處理圓角
● 使用代碼手動(dòng)生成圓角Image設(shè)置到要顯示的View上舶沿,利用UIBezierPath(CoreGraphics框架)畫出來(lái)圓角圖片