轉(zhuǎn)載自:iOS 高效添加圓角效果 性能優(yōu)化 實(shí)戰(zhàn)講解
圓角(RounderCorner)是一種很常見的視圖效果,相比于直角,它更加柔和優(yōu)美,易于接受。但很多人并不清楚如何設(shè)置圓角的正確方式和原理街望。設(shè)置圓角會帶來一定的性能損耗,如何提高性能是另一個需要重點(diǎn)討論的話題弟跑。我查閱了一些現(xiàn)有的資料灾前,收獲良多的同時也發(fā)現(xiàn)了一些誤導(dǎo)人錯誤。本文總結(jié)整理了一些知識點(diǎn)孟辑,概括如下:
- 設(shè)置圓角的正確姿勢及其原理
- 設(shè)置圓角的性能損耗
- 其他設(shè)置圓角的方法豫柬,以及最優(yōu)選擇
我為本文制作了一個 demo,讀者可以在我的 github 上 clone 下來:CornerRadius扑浸,如果覺得有幫助還望給個star以示支持烧给。項(xiàng)目由 Swift 實(shí)現(xiàn),但請務(wù)必相信我即使你只會 Objective-C喝噪,也可以看懂它础嫡。因?yàn)槠渲械年P(guān)鍵知識與 Swift 無關(guān)。
我為本文制作了一個 demo酝惧,讀者可以在我的 github 上 clone 下來:CornerRadius榴鼎,如果覺得有幫助還望給個star以示支持。項(xiàng)目由 Swift 實(shí)現(xiàn)晚唇,但請務(wù)必相信我即使你只會 Objective-C巫财,也可以看懂它。因?yàn)槠渲械年P(guān)鍵知識與 Swift 無關(guān)哩陕。
正確姿勢
首先平项,我想要聲明的一點(diǎn)是:設(shè)置圓角很簡單赫舒,它不會帶來任何性能損耗。
因?yàn)檫@件事本來就很簡單闽瓢,它只需要一行代碼:
view.layer.cornerRadius = 5
先別急著關(guān)掉網(wǎng)頁接癌,也別急著回復(fù),我們讓事實(shí)說話扣讼。打開 Instuments缺猛,選擇 Core Animation 調(diào)試,你會發(fā)現(xiàn)既沒有 Off-Screen Render椭符,也沒有降低幀數(shù)荔燎。關(guān)于使用 Instuments 分析應(yīng)用,你可以參考我的這篇文章:UIKit性能調(diào)優(yōu)實(shí)戰(zhàn)講解销钝。從截圖中可以看到第三個棕色視圖確確實(shí)實(shí)設(shè)置了圓角:
不過查看一下代碼可以發(fā)現(xiàn)有咨,有一個 UILabel 也設(shè)置了圓角,但是沒有表現(xiàn)出任何變化曙搬。關(guān)于這一點(diǎn),你可以查看 cornerRadius 屬性的注釋:
By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
也就是說在默認(rèn)情況下鸽嫂,這個屬性只會影響視圖的背景顏色和 border纵装。對于 UILabel 這樣內(nèi)部還有子視圖的控件就無能為力了。所以很多情況下我們會看到這樣的代碼:
label.layer.cornerRadius = 5
label.layer.masksToBounds = true
我們把第二行代碼添加到 CustomTableViewCell 的構(gòu)造方法中据某,再次運(yùn)行 Instument橡娄,就可以看到圓角效果了。
性能損耗
如果你勾選上 Color Offscreen-Rendered Yellow癣籽,就會發(fā)現(xiàn) label 的四周出現(xiàn)了黃色的標(biāo)記挽唉,說明這里出現(xiàn)了離屏渲染。關(guān)于離屏渲染的介紹筷狼,同樣可以參考:UIKit性能調(diào)優(yōu)實(shí)戰(zhàn)講解瓶籽,就不在本文贅述了。
需要強(qiáng)調(diào)的一點(diǎn)是埂材,離屏渲染并非由設(shè)置圓角導(dǎo)致的塑顺!通過控制變量的方法很容易得出這個結(jié)論,因?yàn)?UIView 只是設(shè)置了 cornerRadius俏险,但它沒有出現(xiàn)離屏渲染严拒。某些比較權(quán)威的文章,比如 Stackoverflow 和 CodeReview 都提到設(shè)置 cornerRadius 會導(dǎo)致離屏渲染從而影響性能竖独,我想這實(shí)在是冤枉了可愛的 cornerRadius 變量裤唠,也誤導(dǎo)了別人。
雖然設(shè)置 masksToBounds 會導(dǎo)致離屏渲染莹痢,從而影響性能种蘸,但是這個影響到底會有多大墓赴?在我的 iPhone6 上,即使出現(xiàn)了 17 個帶有圓角的視圖劈彪,滑動時的幀數(shù)依然在 58 - 59 fps 左右波動竣蹦。
然而,這并非說明 iOS 9 做了什么特殊優(yōu)化沧奴,或者是離屏渲染的影響不大痘括,其主要原因在于圓角不夠多。當(dāng)我將一個 UIImageView 也設(shè)置成圓角滔吠,也就是屏幕上的圓角視圖達(dá)到 34 個時纲菌,fps 大幅度下降,大約只有 33 左右疮绷『采啵基本上已經(jīng)達(dá)到了影響用戶體驗(yàn)的范圍。因此冬骚,一切不講依據(jù)的優(yōu)化都是耍流氓椅贱,如果你的圓角視圖不多,cell 不復(fù)雜只冻,就不要費(fèi)力氣折騰了庇麦。
高效地設(shè)置圓角
假設(shè)現(xiàn)在圓角視圖非常多(比如在 UICollectionView 中),那么如何為視圖高效的添加圓角呢喜德?網(wǎng)上的教程大多沒有說全山橄,因?yàn)檫@個事要分兩種情況考慮。為普通的 UIView 設(shè)置圓角舍悯,和為 UIImageView 設(shè)置圓角的原理截然不同航棱。
有一種做法是這樣的,這種寫法試圖實(shí)現(xiàn) cornerRadius = 3 的效果:
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
}
不過這是一種錯的離譜的寫法萌衬!
首先饮醇,我們應(yīng)該盡量避免重寫 drawRect 方法。不恰當(dāng)?shù)氖褂眠@個方法會導(dǎo)致內(nèi)存暴增秕豫。舉個例子驳阎,iPhone6 上與屏幕等大的 UIView,即使重寫一個空的 drawRect 方法馁蒂,它也至少占用 750 * 1134 * 4 字節(jié) ≈ 3.4 Mb 的內(nèi)存呵晚。在內(nèi)存惡鬼drawRect 及其后續(xù)中,作者詳細(xì)介紹了其中原理沫屡,據(jù)他測試饵隙,在 iPhone6 上空的、與屏幕等大的視圖重寫 drawRect 方法會消耗 5.2 Mb 內(nèi)存沮脖〗鹈總之芯急,能避免重寫 drawRect 方法就盡可能避免。
其次驶俊,這種方法本質(zhì)上是用遮罩層 mask 來實(shí)現(xiàn)娶耍,因此同樣無可避免的會導(dǎo)致離屏渲染。我試著將此前 34 個視圖的圓角改用這種方法實(shí)現(xiàn)饼酿,結(jié)果 fps 掉到 11 左右榕酒。已經(jīng)屬于卡出翔的節(jié)奏了。
忘掉這種寫法吧故俐,下面介紹正確的高效設(shè)置圓角的姿勢想鹰。
為 UIView 添加圓角
這種做法的原理是手動畫出圓角。雖然我們之前說過药版,為普通的視圖直接設(shè)置 cornerRadius 屬性即可辑舷。但萬一不可避免的需要使用 masksToBounds,就可以使用下面這種方法槽片,它的核心代碼如下:
func kt_drawRectWithRoundedCorner(radius radius: CGFloat,
borderWidth: CGFloat,
backgroundColor: UIColor,
borderColor: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale)
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 開始位置); // 開始坐標(biāo)右邊開始
CGContextAddArcToPoint(context, x1, y1, x2, y2, radius); // 這種類型的代碼重復(fù)四次`
CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
let output = UIGraphicsGetImageFromCurrentImageContext();`
UIGraphicsEndImageContext();`
return output
}
這個方法返回的是 UIImage何缓,也就是說我們利用 Core Graphics 自己畫出了一個圓角矩形。除了一些必要的代碼外还栓,最核心的就是 CGContextAddArcToPoint 函數(shù)碌廓。它中間的四個參數(shù)表示曲線的起點(diǎn)和終點(diǎn)坐標(biāo),最后一個參數(shù)表示半徑蝙云。調(diào)用了四次函數(shù)后氓皱,就可以畫出圓角矩形路召。最后再從當(dāng)前的繪圖上下文中獲取圖片并返回勃刨。
有了這個圖片后,我們創(chuàng)建一個 UIImageView 并插入到視圖層級的底部:
extension UIView {
func kt_addCorner(radius radius: CGFloat,
borderWidth: CGFloat,
backgroundColor: UIColor,
borderColor: UIColor) {
let imageView = UIImageView(image: kt_drawRectWithRoundedCorner(radius: radius,
borderWidth: borderWidth,
backgroundColor: backgroundColor,
borderColor: borderColor))
self.insertSubview(imageView, atIndex: 0)
}
}
完整的代碼可以在項(xiàng)目中找到股淡,使用時身隐,你只需要這樣寫:
let view = UIView(frame: CGRectMake(1,2,3,4))
view.kt_addCorner(radius: 6)
為 UIImageView 添加圓角
相比于上面一種實(shí)現(xiàn)方法,為 UIImageView 添加圓角更為常用唯灵。它的實(shí)現(xiàn)思路是直接截取圖片:
extension UIImage {
func kt_drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage {
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
CGContextAddPath(UIGraphicsGetCurrentContext(),
UIBezierPath(roundedRect: rect,
byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: radius,
height: radius)).CGPath)
CGContextClip(UIGraphicsGetCurrentContext())
self.drawInRect(rect)
CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
let output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output
}
}
圓角路徑直接用貝塞爾曲線繪制贾铝,一個意外的 bonus 是還可以選擇哪幾個角有圓角效果。這個函數(shù)的效果是將原來的 UIImage 剪裁出圓角埠帕。配合著這函數(shù)垢揩,我們可以為 UIImageView 拓展一個設(shè)置圓角的方法:
extension UIImageView {
/**
!!!只有當(dāng) imageView 不為nil 時,調(diào)用此方法才有效果
:param: radius 圓角半徑
*/
override func kt_addCorner(radius radius: CGFloat) {
self.image = self.image?.kt_drawRectWithRoundedCorner(radius: radius, self.bounds.size)
}
}
完整的代碼可以在項(xiàng)目中找到敛瓷,使用時叁巨,你只需要這樣寫:
let imageView = let imgView1 = UIImageView(image: UIImage(name: ""))
imageView.kt_addCorner(radius: 6)
提醒:
無論使用上面哪種方法,你都需要小心使用背景顏色呐籽。因?yàn)榇藭r我們沒有設(shè)置 masksToBounds锋勺,因此超出圓角的部分依然會被顯示蚀瘸。因此,你不應(yīng)該再使用背景顏色庶橱,可以在繪制圓角矩形時設(shè)置填充顏色來達(dá)到類似效果贮勃。
在為 UIImageView 添加圓角時,請確保 image 屬性不是 nil苏章,否則這個設(shè)置將會無效寂嘉。
實(shí)戰(zhàn)測試
回到 demo 中,測試一下剛剛定義的這兩個設(shè)置圓角的方法布近。首先在 setupContent 方法中把這兩行代碼的注釋取消掉:
imgView1.kt_addCorner(radius: 5)
imgView2.kt_addCorner(radius: 5)
然后使用自定義的方法為 label 和 view 設(shè)置圓角:
view.kt_addCorner(radius: 6)
label.kt_addCorner(radius: 6)
現(xiàn)在垫释,我們不僅成功的添加了圓角效果,同時還保證了性能不受影響:
性能測試
總結(jié)
- 如果能夠只用 cornerRadius 解決問題撑瞧,就不用優(yōu)化棵譬。
- 如果必須設(shè)置 masksToBounds,可以參考圓角視圖的數(shù)量预伺,如果數(shù)量較少(一頁只有幾個)也可以考慮不用優(yōu)化订咸。
- UIImageView 的圓角通過直接截取圖片實(shí)現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實(shí)現(xiàn)酬诀。
參考資料
轉(zhuǎn)載自 http://www.cocoachina.com/ios/20160301/15486.html 支持原創(chuàng)