今天來寫一個老生常談的話題,也是一個面試的高頻問題,我也在面試時不止一次被問到過這個問題——如何高性能的設(shè)置圓角。就用他作為2017年春節(jié)上班之后的第一篇文章。
起因
在談及圓角這個話題之前,我們必須先知道系統(tǒng)的API是怎樣去簡單方便的設(shè)置圓角的。以一個imageView控件來舉例。
imageView.layer.cornerRadius = CGFloat(10);
簡單粗暴,就能設(shè)置圓角。而在這里的一行代碼,必須為它洗白一件事情,設(shè)置圓角的這行代碼,本身并不會帶來任何的性能損耗。如果諸位看官看到此處不相信,大可打開Instruments用Core Animation來試試看,你就會發(fā)現(xiàn)既沒有Off-Screen Render,也不會出現(xiàn)掉幀的情況。至于使用Instruments來對UIKit進行分析調(diào)試,到時候再寫一篇文章來詳解好了。
但是,如果你給一個UILabel也使用了上面的一行代碼,你會發(fā)現(xiàn)這個UILabel并不會有任何的變化,可是我們確實實實在在的為它設(shè)置了圓角屬性。也就是說,很多時候這個屬性對于內(nèi)部還有子視圖的控件是無能為力的。所以很多時候,我們會這么來設(shè)置圓角。
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
這時候咱們再打開Instruments去觀察,惡心的離屏渲染如約而至。
這里我在稍微贅述一下離屏渲染的概念,什么是離屏渲染呢?
討論造成離屏渲染的原因之前,先說明什么是離屏渲染:離屏渲染指的是在圖像在繪制到當(dāng)前屏幕前,需要先進行一次渲染,之后才繪制到當(dāng)前屏幕。在第一次渲染時,GPU(Core Animation)或CPU(Core Graphics)需要額外的一塊內(nèi)存來進行渲染,完成后再繪制到屏幕。offscreen到onscreen需要進行上下文切換,這個切換的性能消耗是昂貴的。
因此,我們必須避免不必要的離屏渲染。
造成離屏渲染的原因有:
設(shè)置CALayer的cornerRadius,edgeAntialiasingMask,allowsEdgeAntialiasing屬性
把CALayer的maskToBounds設(shè)為YES
設(shè)置CALayer的shadow屬性
設(shè)置CALayer的mask屬性
把CALayer的allowsGroupOpacity屬性設(shè)為YES而且opacity小于1
講到這里,大家大可不必對離屏渲染產(chǎn)生巨大的恐慌,因為當(dāng)一個界面的圓角圖片不夠多的時候,對性能的損耗影響基本可以忽略不計。所以這里的圓角優(yōu)化是針對一屏有很多個圓角的應(yīng)用來說的。
UIImageView 添加圓角
一般我們最常見的是為UIImageView添加圓角,首先重要的事情放到前面講,千萬避免通過重寫drawRect方法來設(shè)置圓角,不恰當(dāng)?shù)氖褂眠@個方法,會導(dǎo)致內(nèi)存的暴增。其次,這種方法的同樣會導(dǎo)致離屏渲染。
而一個比較理想的實現(xiàn)思路,是直接截取圖片。
CGSize size = self.bounds.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius);
UIGraphicsBeginImageContextWithOptions(size, YES, scale);
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (nil == currentContext) {
return;
}
UIBezierPath *cornerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCornerType cornerRadii:cornerRadii];
UIBezierPath *backgroundRect = [UIBezierPath bezierPathWithRect:self.bounds];
[backgroundColor setFill];
[backgroundRect fill];
[cornerPath addClip];
[self.layer renderInContext:currentContext];
[self drawBorder:cornerPath];
UIImage *processedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (processedImage) {
objc_setAssociatedObject(processedImage, &kProcessedImage, @(1), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
self.image = processedImage;
上面這段代碼我只是給出了大致的實現(xiàn)思路,圓角路徑直接用貝塞爾曲線繪制,而其中的屬性,使用了runtime的黑魔法去設(shè)置,在Category 給一個現(xiàn)有的類添加屬性,但是卻不能添加實例變量,這似乎成為了 Objective-C的一個明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects來彌補這一不足。
至于完整的Demo和方法庫,網(wǎng)上已經(jīng)有很多了,Github動手搜索吧。
總結(jié)
如果能夠只用 cornerRadius 解決問題,就不用優(yōu)化。
如果必須設(shè)置 masksToBounds,可以參考圓角視圖的數(shù)量,如果數(shù)量較少(一頁只有幾個)也可以考慮不用優(yōu)化。
UIImageView 的圓角通過直接截取圖片實現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實現(xiàn)。