iOS 圓角頭像優化解決方案

此處輸入圖片的描述
此處輸入圖片的描述

項目中我們會用到頭像或是在視圖展示時,設置“友好”“不尖銳”的圓角,進行美化,當視圖的渲染幀率低于 40 幀每秒,普通用戶就會察覺明顯的不流暢了。視圖和圓角的大小對幀率的影響微乎其微,真正影響幀率的是圓角圖片的數量,數量越多,對于性能的影響是非常大的。

通常我們設置頭像圓角的方法:

yourImageView.layer.cornerRadius = aImageView.frame.size.width/2.0;  
yourImageView.layer.masksToBounds = YES;

使用這個方法會造成 Off-Screen Rendering(離屏渲染)。頻繁發生離屏渲染是非常耗時的,所以會造成幀率會下降,造成卡頓的情況,對性能上造成影響。

Off-Screen Rendering

離屏渲染,指的是 GPU (圖形處理器)在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。道離屏渲染耗時是發生在離屏這個動作上面,而不是渲染。為什么離屏這么耗時?原因主要有創建緩沖區和上下文切換。創建新的緩沖區代價都不算大,付出最大代價的是上下文切換。

上下文切換

上下文切換,不管是在GPU (圖形處理器)渲染過程中,還是一直所熟悉的進程切換,上下文切換在哪里都是一個相當耗時的操作。首先我要保存當前屏幕渲染環境,然后切換到一個新的繪制環境,申請繪制資源,初始化環境,然后開始一個繪制,繪制完畢后銷毀這個繪制環境,如需要切換到On-Screen Rendering或者再開始一個新的離屏渲染重復之前的操作。 下圖描述了一次mask的渲染操作。

此處輸入圖片的描述
此處輸入圖片的描述

批注: 離屏渲染上下文,個人理解,視圖切換,會發生離屏渲染,造成耗費時間,影響性能。

如果 shouldRasterize 被設置成 YES,在觸發離屏繪制的同時,會將光柵化后的內容緩存起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接復用。這將在很大程度上提升渲染性能。

一次mask發生了兩次離屏渲染和一次主屏渲染。即使忽略昂貴的上下文切換,一次mask需要渲染三次才能在屏幕上顯示,這已經是普通視圖顯示3陪耗時,若再加上下文環境切換,一次mask就是普通渲染的30倍以上耗時操作。問我這個30倍以上這個數據怎么的出來的?當我在cell的UIImageView的實例增加到150個,并去掉圓角的時候,幀數才跌至28幀每秒。雖然不是甚準確,但至少反映mask這個耗時是無mask操作的耗時的數十倍的。

解決方案:

//設置圓角視圖樣式
-(void)setUpSelfView{
    //陰影 Shadow
    self.layer.shadowColor = [UIColor blackColor].CGColor; //黑
    self.layer.shadowOpacity = 0.33;//陰影的不透明度
    self.layer.shadowOffset = CGSizeMake(0, 1.5);//陰影的偏移
    self.layer.shadowRadius = 4.0;//陰影半徑
    self.layer.shouldRasterize = YES; //圓角緩存
    self.layer.rasterizationScale = [UIScreen mainScreen].scale;//柵格化圖層 提高流暢度
    //圓角
    self.layer.cornerRadius = 10.0f;
}

這樣大部分情況下可以馬上挽救你的幀數在55幀每秒以上。shouldRasterize = YES會使視圖渲染內容被緩存起來,下次繪制的時候可以直接顯示緩存,當然要在視圖內容不改變的情況下。

除了上面非要作死的人外,大家還是采取預先生成圓角圖片,并緩存起來這個方法才是比較好的手段。預處理圓角圖片可以在后臺處理,處理完畢后緩存起來,再在主線程顯示,這就避免了不必要的離屏渲染了。

另外也有在圖片上面覆蓋一個鏤空圓形圖片的方法可以實現圓形頭像效果,這個也是極為高效的方法。缺點就是對視圖的背景有要求,單色背景效果就最為理想。

使用貝塞爾曲線繪制, 解決離屏渲染問題,推薦

- (UIImage *)imageWithCornerRadius:(CGFloat)radius {

CGRect rect = (CGRect){0.f, 0.f, self.size};
UIGraphicsBeginImageContextWithOptions(self.size, NO, UIScreen.mainScreen.scale);
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
 
[self drawInRect:rect];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 
UIGraphicsEndImageContext();
 
return image;
}

總結

實現圓角cornerRadius要比mask高效很多。Rasterize在大部分情況下極大減少GPU工作。在有空間的情況下,大部分情況下緩存總能幫到你,不是嗎?后臺預處理圖片也能很簡單幫上你很大的忙。


其中shouldRasterize(光柵化)是比較特別的一種:

光柵化概念:將圖轉化為一個個柵格組成的圖象。光柵化特點:每個元素對應幀緩沖區中的一像素。shouldRasterize = YES
在其他屬性觸發離屏渲染的同時,會將光柵化后的內容緩存起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接復用。shouldRasterize
= YES,這將隱式的創建一個位圖,各種陰影遮罩等效果也會保存到位圖中并緩存起來,從而減少渲染的頻度(不是矢量圖)。 相當于光柵化是把GPU的操作轉到CPU上了,生成位圖緩存,直接讀取復用。 當你使用光柵化時,你可以開啟“Color Hits Green
and Misses Red”來檢查該場景下光柵化操作是否是一個好的選擇。綠色表示緩存被復用,紅色表示緩存在被重復創建。
如果光柵化的層變紅得太頻繁那么光柵化對優化可能沒有多少用處。位圖緩存從內存中刪除又重新創建得太過頻繁,紅色表明緩存重建得太遲。可以針對性的選擇某個較小而較深的層結構進行光柵化,來嘗試減少渲染時間。
注意:對于經常變動的內容,這個時候不要開啟,否則會造成性能的浪費 例如我們日程經常打交道的TableViewCell,因為TableViewCell的重繪是很頻繁的(因為Cell的復用),如果Cell的內容不斷變化,則Cell需要不斷重繪,如果此時設置了cell.layer可光柵化。則會造成大量的離屏渲染,降低圖形性能。
所以當使用離屏渲染的時候會很容易造成性能消耗,因為在OPENGL里離屏渲染會單獨在內存中創建一個屏幕外緩沖區并進行渲染,而屏幕外緩沖區跟當前屏幕緩沖區上下文切換是很耗性能的。

為什么會使用離屏渲染
當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染被喚起。
屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。

iOS版本上的優化

iOS 9.0 之前UIimageView跟UIButton設置圓角都會觸發離屏渲染
iOS 9.0 之后UIButton設置圓角會觸發離屏渲染,而UIImageView里png圖片>設置圓角不會觸發離屏渲染了,如果設置其他陰影效果之類的還是會觸發離屏渲染的。
這可能是蘋果也意識到離屏渲染會產生性能問題,所以能不產生離屏渲染的地方蘋果也就不用離屏渲染了。

感謝:

http://www.cocoachina.com/ios/20150803/12873.html
http://blog.csdn.net/qxuewei/article/details/50953950
http://foggry.com/blog/2015/05/06/chi-ping-xuan-ran-xue-xi-bi-ji/?utm_source=tuicool&utm_medium=referral
https://zsisme.gitbooks.io/ios-/content/chapter15/offscreen-rendering.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容