原文
寫在前面
iOS設置圓角的性能探究已經是一個老生常談的問題了,眾所周知,如果直接使用layer的cornerRadius + masksToBounds雖然可以很方便的完成圓角設置,但會引起離屏渲染,導致性能問題,在列表視圖中過多的圓角設置就會導致滑動卡頓,現在主流的方案就是在獲取圖片的一刻開啟異步線程對圖片進行相應的圓角處理,把圖片處理成想要的圖片在返回主線程進行顯示,想要便捷的達成此目的推薦YY大神的YYWebImage,其在獲取圖片的時候的時候提供了一個transform的block,在此block中你可以完成圖片的處理工作,但是在實際的使用中,我覺得第二種方案還是有些不方便的地方,具體如下:
1、對於網絡圖片,大多數要提前設置展位圖,如果美工沒有提供圓角占位圖片,你需要相應的對占位圖片進行圓角處理;
2、對於混合視圖需要圓角的,比如下圖,圖片上有一個Label,label也需要圓角化,你也得對label進行單獨的處理(這裡我有個小tip:如果必須使用這種方式,我的做法是生成一張左下角和右下角圓角化的黑色背景圖片,然後使用colorWithPatternImage 將圖片設置label的背景色,這樣你不需要為這個黑色的圓角地圖另外創建一個視圖)
圖片上有Label.png
3、如果多處需要重復使用同一個圖片地址,使用YYWebImage時,其會將tranform後的圖片緩存起來,所以就會出現,如果你在一個地方圓角化了該圖片,在另一個地方使用時依然會是圓角化的圖片,這顯然在有時候是不滿足需求的,不過你可以使用不同的YYWebImageManager來管理相同圖片地址而需要不同transform的圖片;
綜上所述,雖然可以通過一些方式解決上述問題,如果有一個性能優秀且能避免上訴問題產生的圓角化方案就更好了。
我的方案
要避免上述問題,我們就不能從修改圖片入手了,還是需要從視圖層次入手,我采取的方案其實也相當簡單,如果某個視圖需要圓角化,我只需要在該視圖上添加一個子layer到最上層,用於遮蓋該視圖及其子視圖,設置layer的圖片為剛好能夠遮蓋成所需圓角樣子並且圖片顏色剛好是該視圖父視圖的背景顏色就達到達到想要的效果的,由於該遮罩layer在最上層,所以對於上面所提到的第二個缺點中的Label,也順帶著遮罩了,所以無需再次處理,當然由於我們是在視圖層次而非圖片層次處理的圓角,上面的第一個和第三個缺點也不存在了,這樣其實很簡單的解決的上訴三個缺點,下面來看看相關的代碼:
1、首先是繪制遮蓋layer的圖層圖片,當然我們可以讓美工切圖給我們,但是如果對於每個尺寸的視圖都去切圖的話,工作量就相應增大了,其實我們值需要繪制一張如下的圖片,如果是空白,請點擊圖片查看
帶邊框.png
不帶邊框.png
先來看看繪制代碼
/**我創建了一個分類用於創建相應的遮罩圖片*/ @implementation UIImage (XWAddForRoundedCorner) /**提供一個在一個指定的size中繪制圖片的便捷方法*/ + (UIImage *)xw_imageWithSize:(CGSize)size drawBlock:(void (^)(CGContextRef context))drawBlock { if (!drawBlock) return nil; UIGraphicsBeginImageContextWithOptions(size, NO, 0); CGContextRef context = UIGraphicsGetCurrentContext(); if (!context) return nil; drawBlock(context); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } /**繪制方法的具體邏輯,遮罩圖片的邏輯是繪制一個矩形,然後在繪制一個相應的圓角矩形,然後填充矩形和圓角矩形的中間部分為父視圖的背景色*/ + (UIImage *)xw_maskRoundCornerRadiusImageWithColor:(UIColor *)color cornerRadii:(CGSize)cornerRadii size:(CGSize)size corners:(UIRectCorner)corners borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth{ return [UIImage xw_imageWithSize:size drawBlock:^(CGContextRef _Nonnull context) { CGContextSetLineWidth(context, 0); [color set]; CGRect rect = CGRectMake(0, 0, size.width, size.height); //繪制一個矩形,這裡發-0.3是為了防止邊緣的鋸齒, UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:CGRectInset(rect, -0.3, -0.3)]; //繪制圓角矩形,這裡的0.3是為了防止內邊框的鋸齒 UIBezierPath *roundPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, 0.3, 0.3) byRoundingCorners:corners cornerRadii:cornerRadii]; [rectPath appendPath:roundPath]; CGContextAddPath(context, rectPath.CGPath); //注意要用EOFill方式進行填充而非Fill方式 CGContextEOFillPath(context); //如下是繪制邊框,原理依舊是繪制一個外邊框然後根據邊框寬度繪制一個內邊框同樣采取EOFill的方式進行填充即可 if (!borderColor || !borderWidth) return; [borderColor set]; UIBezierPath *borderOutterPath = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:cornerRadii]; UIBezierPath *borderInnerPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:cornerRadii]; [borderOutterPath appendPath:borderInnerPath]; CGContextAddPath(context, borderOutterPath.CGPath); CGContextEOFillPath(context); }]; } @end
上述的繪制方法充分利用了系統繪制圓角的方法bezierPathWithRoundedRect + EOFill,其實最開始我是考慮自己繪制四分之一的圓弧來構建圓角的,後來發現不但代碼多了不少,而且繪制出來的圓角始終沒有系統這個繪制方法的圓潤,我打印的系統的圓角路徑,發現其應該不是四分之一圓弧,發現他的控制點取值有些奇怪,我也不太清楚是如何取的,但是效果的確要好一點,總之采取如上的EOFill方式取巧,繪制的圓角圖片可以達到和系統的cornerRadius完全相同的效果!
2、對於繪制的圖片,我們應該進行保存,當遇到顏色,圓角以及邊框等屬性完全相同的繪制請求時候,我們可以及時復用,避免多次創建
優缺點
優點的話,主要是避免了去解決上面說道提到的幾個問題,如果你有遇到上面3個問題的困擾,我覺得這是相當不錯的方案
再羅列缺點:
1、由於創建圖片需要一個背景色,該背景色源於需要遮蓋視圖的父視圖的顏色,如果該父視圖的顏色不是純色或者存在透明的話,此時該方式就不適用了,此時還是應該采取處理圖片的方式來解決;
2、如果父視圖的顏色會變化,比如點擊cell的時候,此刻你需要同時更新遮罩圖片為相應顏色的圖片,所以需要多寫一些代碼。
封裝
對於此方案,我封裝了一個簡單的UIView的分類UIView+XWAddForRoundedCorner來達到目的,github地址是XWCornerRadius ,此分類分成簡單只有3個API ,且代碼只有200行,沒有其它依賴,具體API如下:
/** 設置一個四角圓角 @param radius 圓角半徑 @param color 圓角背景色 */ - (void)xw_roundedCornerWithRadius:(CGFloat)radius cornerColor:(UIColor *)color; /** 設置一個普通圓角 @param radius 圓角半徑 @param color 圓角背景色 @param corners 圓角位置 */ - (void)xw_roundedCornerWithRadius:(CGFloat)radius cornerColor:(UIColor *)color corners:(UIRectCorner)corners; 44 /** 設置一個帶邊框的圓角 44 @param cornerRadii 圓角半徑cornerRadii @param color 圓角背景色 @param corners 圓角位置 @param borderColor 邊框顏色 @param borderWidth 邊框線寬 */ - (void)xw_roundedCornerWithCornerRadii:(CGSize)cornerRadii cornerColor:(UIColor *)color corners:(UIRectCorner)corners borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth;
你只需要調用相應的API就能完成一個圓角 + 邊框的遮罩效果,相同遮罩圖片的復用我也做了相關處理了,具體使用如下:
[headerView xw_roundedCornerWithCornerRadii:XWSizeMake(40, 40) cornerColor:[UIColor whiteColor] corners:UIRectCornerAllCorners borderColor:[UIColor redColor] borderWidth:widthRatio(2)];
demo列表.gif
寫在最後
對於此方法我已經做了簡單的測試,性能還是相當不錯的,雖然有圖層混合,但是相對於離屏渲染,都是小問題,如上圖在我的老ipod的上也相當流暢,大家可以自行嘗試,更多的關於這幾種方案的性能比較,網上也已經很多了,大家可以自行查找,當然你也可以使用YYWebImage來處理圖片的尺寸來進一步優化圖片的展示, 如果在某些非常復雜的場景想要進一步提高流暢度,YYKit的源碼以及 YY大神關於性能的相關文章絕對值得反復閱讀,不過對於優化的態度,我覺得還是先考慮需求的實現,再來考慮性能優化最好,所謂過早的性能優化都是魔鬼嘛~~~對於該圓角思路如果有疑問歡迎提出,如果覺得有幫助,感謝star,再復習一遍github地址:XWCornerRadius