iOS-高效設定圓角

b10l07發表於2017-08-24

一、前因

CALayer由背景色backgroundColor、內容contents、邊緣borderWidth&borderColor構成

  • 設定圓角不就是設定layer的cornerRadius嗎,還談什麼高效?
    因為這個屬性只會影響檢視的背景顏色和 border。所以該方法只對UIView有效,對於 UIImageView 這樣內部還有子檢視的控制元件就無能為力了。
  • 所以很多情況下我們會加上layer.masksToBounds的設定。

這樣圓角效果就有了。但是,如果你勾選上 Color Offscreen-Rendered Yellow,就會發現 label 的四周出現了黃色的標記,說明這裡出現了離屏渲染。關於離屏渲染的介紹,可以參考:UIKit效能調優實戰講解

之前有的文章說 iOS 9 做了什麼特殊優化,或者是離屏渲染的影響不大,其主要原因在於圓角不夠多。當我將一個 UIImageView 也設定成圓角,也就是螢幕上的圓角檢視達到 34 個時,fps 大幅度下降,大約只有 33 左右。基本上已經達到了影響使用者體驗的範圍。因此,一切不講依據的優化都是耍流氓,如果你的圓角檢視不多,cell 不復雜,就不要費力氣折騰了。

二、首先,來個錯誤示範:

override func drawRect(rect: CGRect) {  
    let maskPath = UIBezierPath(roundedRect: rect,
                                byRoundingCorners: .AllCorners,
                                cornerRadii: CGSize(width: 3, height: 3))
    let maskLayer = CAShapeLayer()
    maskLayer.frame = self.bounds
    maskLayer.path = maskPath.CGPath
    self.layer.mask = maskLayer
}
  • 首先,我們應該儘量避免重寫 drawRect
    方法。不恰當的使用這個方法會導致記憶體暴增。舉個例子,iPhone6 上與螢幕等大的 UIView
    ,即使重寫一個空的 drawRect
    方法,它也至少佔用 750 * 1134 * 4 位元組 ≈ 3.4 Mb
    的記憶體。在 記憶體惡鬼drawRect 及其後續中,作者詳細介紹了其中原理,據他測試,在 iPhone6 上空的、與螢幕等大的檢視重寫 drawRect
    方法會消耗 5.2 Mb 記憶體。總之,能避免重寫 drawRect
    方法就儘可能避免。
  • 其次,這種方法本質上是用遮罩層 mask
    來實現,因此同樣無可避免的會導致離屏渲染。我試著將此前 34 個檢視的圓角改用這種方法實現,結果 fps 掉到 11 左右。已經屬於卡出翔的節奏了。

三、實戰:設定圓角的正確姿勢

1.UIView設定圓角

對於 contents 無內容或者內容的背景透明(無涉及到圓角以外的區域)的layer,直接設定layer的 backgroundColor 和 cornerRadius 屬性來繪製圓角:

  1. UIView的contents無內容可以直接通過設定cornerRadius達到效果。
  2. UILable的contents也一樣,所以也可通過設定cornerRadius達到效果。不過label不能直接設定backgroundColor,因為這樣設定的是contents的backgroundColor,需要設定layer. backgroundColor

前面提到過UIView通過cornerRadius就可以,但是如果特殊情況需要設定layer.masksToBounds,就不要通過cornerRadius方式了,會用到如下方式:

@implementation UIView (RounderCorner)

- (void)dlj_addRounderCornerWithRadius:(CGFloat)radius size:(CGSize)size
{
    UIGraphicsBeginImageContextWithOptions(size, NO, 0);
    CGContextRef cxt = UIGraphicsGetCurrentContext();
    
    CGContextSetFillColorWithColor(cxt, [UIColor redColor].CGColor);
    CGContextSetStrokeColorWithColor(cxt, [UIColor redColor].CGColor);
    
    CGContextMoveToPoint(cxt, size.width, size.height-radius);
    CGContextAddArcToPoint(cxt, size.width, size.height, size.width-radius, size.height, radius);//右下角
    CGContextAddArcToPoint(cxt, 0, size.height, 0, size.height-radius, radius);//左下角
    CGContextAddArcToPoint(cxt, 0, 0, radius, 0, radius);//左上角
    CGContextAddArcToPoint(cxt, size.width, 0, size.width, radius, radius);//右上角
    CGContextClosePath(cxt);
    CGContextDrawPath(cxt, kCGPathFillStroke);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
    [imageView setImage:image];
    [self insertSubview:imageView atIndex:0];
}

這個方法返回的是 UIImage,也就是說我們利用 Core Graphics 自己畫出了一個圓角矩形。除了一些必要的程式碼外,最核心的就是 CGContextAddArcToPoint 函式。它中間的四個參數列示曲線的起點和終點座標,最後一個參數列示半徑。呼叫了四次函式後,就可以畫出圓角矩形。最後再從當前的繪圖上下文中獲取圖片並返回。
有了這個圖片後,我們建立一個 UIImageView 並插入到檢視層級的底部。
使用時,你只需要這樣寫:

[view dlj_addRounderCornerWithRadius:10 size:CGSizeMake(60, 30)];

我這裡只是單純為了實現圓角,當然大家在用的時候可以新增背景顏色、以及設定邊框的屬性。

2.ImageView新增圓角

相比於上面一種實現方法,為 UIImageView 新增圓角更為常用。它的實現思路是直接擷取圖片:

@implementation UIImage (ImageRoundedCorner)

- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    CGContextAddPath(ctx,path.CGPath);
    CGContextClip(ctx);
    [self drawInRect:rect];
    CGContextDrawPath(ctx, kCGPathFillStroke);
    UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

圓角路徑直接用貝塞爾曲線繪製,一個意外的 bonus 是還可以選擇哪幾個角有圓角效果。這個函式的效果是將原來的 UIImage 剪裁出圓角。配合著這函式,我們可以為 UIImageView 擴充一個設定圓角的方法來更加方便的使用。

提醒

  • 無論使用上面哪種方法,你都需要小心使用背景顏色。因為此時我們沒有設定 masksToBounds,因此超出圓角的部分依然會被顯示。因此,你不應該再使用背景顏色,可以在繪製圓角矩形時設定填充顏色來達到類似效果。
  • 在為 UIImageView 新增圓角時,請確保 image 屬性不是 nil,否則這個設定將會無效。

四、擴充套件:其他會導致離屏渲染的解決方案

以下離屏渲染操作,按對效能影響等級從高到低進行排序:

1. shadows(陰影)

方案:在設定完layer的shadow屬性之後,設定layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;

2.圓角(前邊已解決過)

3.mask遮罩

方案:不用mask(哈哈)

4. allowsGroupOpacity(組不透明)

開啟CALayer的 allowsGroupOpacity 屬性後,子 layer 在視覺上的透明度的上限是其父 layer 的 opacity (對應UIView的 alpha ),並且從 iOS 7 以後預設全域性開啟了這個功能,這樣做是為了讓子檢視與其容器檢視保持同樣的透明度。
方案:關閉 allowsGroupOpacity 屬性,按產品需求自己控制layer透明度。

5. edge antialiasing(抗鋸齒)

方案:不設定 allowsEdgeAntialiasing 屬性為YES(預設為NO)

6. shouldRasterize(光柵化)

當檢視內容是靜態不變時,設定 shouldRasterize(光柵化)為YES,此方案最為實用方便。

view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;

但當檢視內容是動態變化(如後臺下載圖片完畢後切換到主執行緒設定)時,使用此方案反而為增加系統負荷。

7.Core Graphics API(核心繪圖)

Core Graphics API(核心繪圖)的繪製操作會導致CPU的離屏渲染。
方案:放到後臺執行緒中進行。

參考資料:
iOS 高效新增圓角效果實戰講解
iOS 離屏渲染優化(Offscreen Render)

相關文章