iOS 圓角優(yōu)化-下

圓角是iOS系統(tǒng)中常見的視覺樣式,從系統(tǒng)圖標(biāo)到導(dǎo)航欄按鈕,圓角無處不在。因為圓角是符合人類視覺安全體驗的,圓角讓人覺得舒適,而尖角在潛意識層次是具有傷害體驗的,因為尖尖的東西總是有可能對人造成傷害的,所以我們更喜歡圓角。在iOS開發(fā)過程中,蘋果提供了一種添加圓角的方法,簡單暴力有效,但并不是所有的開發(fā)者都清楚原理,因此設(shè)置圓角有時會帶來一定的性能損耗。

上一篇文章從理論上介紹設(shè)置圓角方法,性能損耗的原因這兩方面展開討論。本文將從具體項目的實踐中介紹如何避免性能損耗。

提升性能的幾種方法

前文已經(jīng)提到,圓角設(shè)置不當(dāng)會導(dǎo)致系統(tǒng)進(jìn)行離屏渲染,進(jìn)而造成卡頓和跳幀。具體到項目實踐中,解決問題的途徑有兩種:一是盡量減小離屏渲染造成的CPU/GPU資源消耗,達(dá)到減緩卡頓的程度,下面將要介紹的光柵化(Rasterization)就是遵循這個思路的解決途徑;二是通過其他方法實現(xiàn)『圓形的角』效果,盡量避免離屏渲染。具體方法包括裁剪圖片,圓角遮罩等。

光柵化

光柵化(Rasterization),將layer渲染為bitmap存儲于幀緩沖區(qū)中(一種特殊的離屏渲染),每個像素對應(yīng)幀緩沖區(qū)中的一像素。開啟光柵化只需下面兩行代碼:

view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = cell.layer.contentsScale;

以下是官方文檔shouldRasterize屬性的解釋:

When the value of this property is YES, the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content. Shadow effects and any filters in the filters property are rasterized and included in the bitmap. However, the current opacity of the layer is not rasterized.

shouldRasterize = YES在其他屬性觸發(fā)離屏渲染的同時,會將光柵化后的內(nèi)容緩存起來,如果對應(yīng)的layer及其sublayers沒有發(fā)生改變,在下一幀的時候可以直接復(fù)用。shouldRasterize = YES,這將隱式的創(chuàng)建一個位圖,各種陰影遮罩等效果也會保存到位圖中并緩存起來,從而減少渲染的頻度(不是矢量圖)。(來源)

開啟光柵化后,GPU 只合成一次內(nèi)容,然后復(fù)用合成的結(jié)果;合成的內(nèi)容超過 100ms 沒有使用會從緩存里移除,在更新內(nèi)容時還會產(chǎn)生更多的離屏渲染。對于內(nèi)容不發(fā)生變化的視圖,原本拖后腿的離屏渲染就成為了助力;如果視圖內(nèi)容是動態(tài)變化的,使用這個方案有可能讓性能變得更糟。

裁剪圖片

我們得到一張圖片之后,對圖片本身進(jìn)行裁剪,把圖片的直角裁剪為圓角。以后每次使用此圖片時,都是一張帶圓角的圖片,這就從源頭控制了后續(xù)獲取圓角的各種操作,非常適用于頻繁動態(tài)更新的場景。

// UIImage+RoudedCorner.m
- (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius
{
    CGFloat w = self.size.width;
    CGFloat h = self.size.height;
    CGFloat scale = [UIScreen mainScreen].scale;
    
    // 防止圓角半徑小于0,或者大于寬/高中較小值的一半。
    if (cornerRadius < 0) {
        cornerRadius = 0;
    }
    else if (cornerRadius > MIN(w, h)) {
        cornerRadius = MIN(w, h) / 2.;
    }
    
    UIImage *image = nil;
    CGRect imageFrame = CGRectMake(0., 0., w, h);
    UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
    [self drawInRect:imageFrame];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}

圓角遮罩

在要添加圓角的視圖上再疊加一個部分透明的視圖,只對圓角部分進(jìn)行遮擋。遮擋的部分背景最好與周圍背景相同。多一個圖層會增加合成的工作量,但這點工作量與離屏渲染相比微不足道,性能上無論各方面都和無效果持平。如果疊加的視圖都一樣,可以只加載一次遮罩圖片以減少內(nèi)存占用。

除了用軟件畫出來保存在項目里,直接用代碼畫出來也是很簡單的。在一個設(shè)置opaque = NO的 CGContext 里,設(shè)定填充顏色然后用兩條曲線圍成一個封閉區(qū)域,最后從這個繪制環(huán)境導(dǎo)出圖像即可。

// UIImage+RoudedCorner.m
/**
 *  生成一張圖片,用于遮蓋UIView的四角,使UIView呈現(xiàn)圓角效果
 *
 *  @param color  遮罩顏色
 *  @param radius 圓角半徑
 *
 *  @return 一張resizable圖片,圖片尺寸(2 * radius + 1) * (2 * radius + 1),中心的1px為透明色,用于拉伸
 */
 + (UIImage *)roundedCornerMaskImageWithColor:(UIColor *)color radius:(CGFloat)radius
{
    if (radius <= 0) {
        return nil;
    }
    
    CGRect rect = CGRectMake(0, 0, radius * 2 + 1, radius * 2 + 1);
    
    UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    [color set];
    
    CGPathRef rectPath = CGPathCreateWithRect(rect, NULL);
    CGContextAddPath(context, rectPath);
    CGPathRef roundedCornerPath = CGPathCreateWithRoundedRect(rect, radius, radius, NULL);
    CGContextAddPath(context, roundedCornerPath);
    CGContextEOFillPath(context);
    
    UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    CGPathRelease(rectPath);
    CGPathRelease(roundedCornerPath);
    
    return [maskImage resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius)];
}

圓角背景圖

生成一張圓角圖片,疊加在目標(biāo)視圖的最底部。效率和圓角遮罩一樣。這種方法特別適用于UIButton,調(diào)用UIButtonsetBackgroundImage:forState:方法,將生成的圖片設(shè)置為背景圖即可。

// UIImage+RoudedCorner.m
- (UIImage *)roundedCorner:(CGFloat)cornerRadius tintedGradientColor:(UIColor *)tintColor
{
    CGFloat w = self.size.width;
    CGFloat h = self.size.height;
    CGFloat scale = [UIScreen mainScreen].scale;
    // 防止圓角半徑小于0,或者大于寬/高中較小值的一半。
    if (cornerRadius < 0)
        cornerRadius = 0;
    else if (cornerRadius > MIN(w, h))
        cornerRadius = MIN(w, h) / 2.;
    
    CGRect imageFrame = CGRectMake(0., 0., w, h);
    UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
    [self drawInRect:imageFrame];
    
    [tintColor setFill];
    UIRectFill(imageFrame);
    [self drawInRect:imageFrame blendMode:kCGBlendModeOverlay alpha:1.0f];
    
    UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return tintedImage;
}

文本視圖類上實現(xiàn)圓角

文本視圖主要是這三類:UILabel, UITextField, UITextView。其中 UITextField 類自帶圓角風(fēng)格的外型,UILabelUITextView 要想顯示圓角需要表現(xiàn)出與周圍不同的背景色才行。想要在 UILabelUITextView 上實現(xiàn)低成本的圓角(不觸發(fā)離屏渲染),需要保證 layercontents呈現(xiàn)透明的背景色,文本視圖類的 layercontents默認(rèn)是透明的(字符就在這個透明的環(huán)境里繪制、顯示),此時只需要設(shè)置 layerbackgroundColor,再加上cornerRadius就可以搞定了。不過 UILabel 上設(shè)置backgroundColor的行為被更改了,不再是設(shè)定 layer 的背景色而是為contents設(shè)置背景色,UITextView 則沒有改變這一點,所以在 UILabel 上實現(xiàn)圓角要這么做:

//不要這么做:label.backgroundColor = aColor 以及不要在 IB 里為 label 設(shè)置背景色
label.layer.backgroundColor = aColor;
label.layer.cornerRadius = 5;

總結(jié)

  1. 光柵化(Rasterization)可以有效提高幀率,前提是layer的contents頻繁更新。
  2. 裁剪圖片可以一勞永逸地解決生成圖片圓角的問題,但僅限于圖片,不能有多個視圖疊加在圖片之上。同時一旦裁剪完成,圓角大小就不可更改,適用范圍很窄。
  3. 圓角遮罩方法在目標(biāo)視圖上再添加一層視圖,可以蓋住底層所有的視圖,可擴(kuò)展性強(qiáng),但要求父視圖是純色背景。
  4. 沒有哪種方法可以cover所有的情況,總有一種情況沒有辦法做任何優(yōu)化,聽天由命吧……
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容