-------》(轉(zhuǎn)發(fā))
這篇文章介紹ZYCornerRadius解決生產(chǎn)中圓角帶來(lái)的離屏渲染問(wèn)題的思路。
日常生產(chǎn)中app布局離不開(kāi)美麗的圓角(RounderCorner),特別是用圓角UIImageView來(lái)做數(shù)據(jù)呈現(xiàn)交互,但是這種柔和易于讓人接受的視圖效果并不僅僅是改變了一個(gè)形狀那么簡(jiǎn)單,需要付出一定的性能代價(jià)。
相信這已經(jīng)是總所周知的問(wèn)題了,日常我們使用layer的兩個(gè)屬性,簡(jiǎn)單的兩行代碼就能實(shí)現(xiàn)圓角的呈現(xiàn)
1
2imageView.layer.cornerRadius?=?CGFloat(10);
imageView.layer.masksToBounds?=?YES;
由于這樣處理的渲染機(jī)制是GPU在當(dāng)前屏幕緩沖區(qū)外新開(kāi)辟一個(gè)渲染緩沖區(qū)進(jìn)行工作,也就是離屏渲染,這會(huì)給我們帶來(lái)額外的性能損耗,如果這樣的圓角操作達(dá)到一定數(shù)量,會(huì)觸發(fā)緩沖區(qū)的頻繁合并和上下文的的頻繁切換,性能的代價(jià)會(huì)宏觀地表現(xiàn)在用戶體驗(yàn)上----掉幀。這也是我親身體驗(yàn)過(guò)的,有一次朋友在玩我手機(jī)的時(shí)候問(wèn)我為什么會(huì)卡,看了后才發(fā)現(xiàn)原來(lái)是一個(gè)充滿圓形頭像的TableView。
屏幕的渲染機(jī)制這里就不copy了,很多朋友的文章也討論過(guò)這樣的問(wèn)題。這篇文章有深入介紹屏幕顯示機(jī)制。這里順便貼一下我筆記里記錄的會(huì)引發(fā)離屏渲染的操作,給大家做個(gè)記憶捆綁,正確與否大家可以自己思量。
The following will trigger offscreen rendering:
Any layer with a mask (layer.mask)
Any layer with layer.masksToBounds / view.clipsToBounds being true
Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
Any layer with a drop shadow (layer.shadow*).
Any layer with layer.shouldRasterize being true
Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing
Text (any kind, including UILabel, CATextLayer, Core Text, etc).
Most of the drawing you do with CGContext in drawRect:. Even an empty implementation will be rendered offscreen.
因?yàn)檫@些效果均被認(rèn)為不能直接呈現(xiàn)于屏幕,而需要在別的地方做額外的處理預(yù)合成。具體的檢測(cè)我們可以使用Instruments的CoreAnimation。
ZYCornerRadius
以下介紹ZYCornerRadius(以Category的方式工作)對(duì)UIImageView設(shè)置圓角會(huì)觸發(fā)離屏渲染的解決思路,有什么問(wèn)題和建議還請(qǐng)大家發(fā)issues指導(dǎo)更正。
先上一張性能對(duì)比圖
測(cè)試設(shè)備6P,屏幕中有40張尺寸為20*20的小圖片,使用masksToBounds切角處理時(shí)幀率大大下降至20+,使用ZYCornerRadius時(shí)幀率保持在57+,性能接近0損耗。

既然我們要避免讓GPU觸發(fā)離屏,那么只能把兵符交給CPU,雖然CPU對(duì)圖形的處理能力不及GPU,但由于這種處理的難度不大,且代價(jià)肯定遠(yuǎn)小于上下文切換。
其實(shí)一開(kāi)始的想法就是從-drawRect下手,但是看了某篇文章(找不回來(lái)了)后打消了這個(gè)念頭,-drawRect的確存在很多性能坑。
既然不能讓控件masksToBounds,ZYCornerRadius就從圖片本身下手,我使用在UIKit中對(duì)Core
Graphics有一定封裝的應(yīng)用層類UIBezierPath,對(duì)圖片進(jìn)行破壞性的切角,破壞性僅僅是對(duì)切去部分而言,當(dāng)然這操作是在CPU內(nèi)完成的,而后我只需要取到處理完成的bitmap(可為UIImage對(duì)象)交給GPU顯示于屏幕即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
*?@brief?clip?the?cornerRadius?with?image,?UIImageView?must?be?setFrame?before,?no?off-screen-rendered
*/
-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?{
CGSize?size?=?self.bounds.size;
CGFloat?scale?=?[UIScreen?mainScreen].scale;
CGSize?cornerRadii?=?CGSizeMake(cornerRadius,?cornerRadius);
UIGraphicsBeginImageContextWithOptions(size,?NO,?scale);
if(nil?==?UIGraphicsGetCurrentContext())?{
return;
}
UIBezierPath?*cornerPath?=?[UIBezierPath?bezierPathWithRoundedRect:self.bounds?byRoundingCorners:rectCornerType?cornerRadii:cornerRadii];
[cornerPath?addClip];
[image?drawInRect:self.bounds];
self.image?=?UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
可見(jiàn),我對(duì)圖片進(jìn)行了切角處理后,將得到的含圓角UIImage通過(guò)-setImage傳給了UIImageView。操作沒(méi)有觸發(fā)GPU離屏渲染,過(guò)程在CPU內(nèi)完成,而后我在Demo中證實(shí)了這個(gè)方法。
順便一提這里還存在一個(gè)性能問(wèn)題,Color
Blended Layers,UIGraphicsBeginImageContextWithOptions(<#cgsize>,
<#bool>, <#cgfloat>)的第二個(gè)參數(shù)是透明通道的開(kāi)關(guān),true則為不透明。以下兩張圖是參數(shù)傳NO or
YES在模擬器中打開(kāi)了Color Blended Layers Debug所看見(jiàn)的區(qū)別:

一些沒(méi)有被設(shè)置為opacity的圖層,因?yàn)橥该魍ǖ赖拇嬖冢到y(tǒng)需要去計(jì)算圖層堆疊后像素點(diǎn)的真實(shí)顏色,在Instruments的測(cè)試中也是可以高亮標(biāo)顯出來(lái),這種性能的損耗程度我還沒(méi)有專門去測(cè)試。但是在上圖可以看見(jiàn)如果設(shè)置為不包含透明通道,我們圖片被剪去的部分就沒(méi)有了顏色(黑漆漆一片),這里使用的解決方案就是在圖片上下文中先畫一層backgroundColor,缺點(diǎn)就是需要傳入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
*?@brief?clip?the?cornerRadius?with?image,?draw?the?backgroundColor?you?want,?UIImageView?must?be?setFrame?before,?no?off-screen-rendered
*/
-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?backgroundColor:(UIColor?*)backgroundColor?{
CGSize?size?=?self.bounds.size;
CGFloat?scale?=?[UIScreen?mainScreen].scale;
CGSize?cornerRadii?=?CGSizeMake(cornerRadius,?cornerRadius);
UIGraphicsBeginImageContextWithOptions(size,?YES,?scale);
if(nil?==?UIGraphicsGetCurrentContext())?{
return;
}
UIBezierPath?*cornerPath?=?[UIBezierPath?bezierPathWithRoundedRect:self.bounds?byRoundingCorners:rectCornerType?cornerRadii:cornerRadii];
UIBezierPath?*backgroundRect?=?[UIBezierPath?bezierPathWithRect:self.bounds];
[backgroundColor?setFill];
[backgroundRect?fill];
[cornerPath?addClip];
[image?drawInRect:self.bounds];
self.image?=?UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
傳入紅色的背景顏色,打開(kāi)Color Blended Layers Debug與原先對(duì)比:

目前我們解決了離屏渲染的問(wèn)題,可這并不符合實(shí)際生產(chǎn),在app中顯示的網(wǎng)絡(luò)圖片我們不可能事先知道并且調(diào)用-
(void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:來(lái)進(jìn)行切角,也不可能每次都還要寫個(gè)SDWedImage的complete回調(diào)去做這個(gè)操作,我決定用swizzleMethod的辦法來(lái)處理,關(guān)于對(duì)swizzleMethod的認(rèn)識(shí),可以看看我這篇文章。
我們把對(duì)self.image切角處理放在每次layoutSubviews的時(shí)候完成,大家看到這里頓時(shí)把我臭罵了一頓。。。在Category里重寫-layoutSubviews的致命的,這的確會(huì)導(dǎo)致整個(gè)項(xiàng)目下所有的UIImageView都會(huì)去執(zhí)行這個(gè)山寨的-layoutSubviews,別慌關(guān)掉文章,給個(gè)機(jī)會(huì)繼續(xù)看下去。
首先我們需要將使用者傳入的切角參數(shù)保存起來(lái),供-layoutSubviews切角時(shí)使用,因?yàn)閏ategory不支持?jǐn)U展屬性,所以我們可以用runtime來(lái)做:
1
2
3
4
5
6
7
8
9/**
*?@brief?set?cornerRadius?for?UIImageView,?no?off-screen-rendered
*/
-?(void)zy_cornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?{
objc_setAssociatedObject(self,?&kRadius,?@(cornerRadius),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self,?&kRoundingCorners,?@(rectCornerType),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self,?&kIsRounding,?@(0),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self.class?swizzleMethod:@selector(layoutSubviews)?anotherMethod:@selector(zy_LayoutSubviews)];
}
細(xì)心的朋友可以看見(jiàn)上面這段代碼里的+swizzleMethod,我將調(diào)用了-
(void)zy_cornerRadiusAdvance:cornerRadius:rectCornerType:的UIImageView對(duì)象的-layoutSubviews方法的實(shí)現(xiàn)轉(zhuǎn)移到了我自己的方法-zy_LayoutSubviews上,也就是說(shuō)我不需要去重寫-layoutSubviews,而主動(dòng)調(diào)用過(guò)-zy_cornerRadiusAdvance的UIImageView對(duì)象的-layoutSubviews的實(shí)現(xiàn)卻被我換成了-zy_LayoutSubviews,源代碼在Demo中有。ok,于是在-zy_LayoutSubviews中收官:
1
2
3
4
5
6-?(void)zy_LayoutSubviews?{
[superlayoutSubviews];
NSNumber?*radius?=?objc_getAssociatedObject(self,?&kRadius);
NSNumber?*roundingCorners?=?objc_getAssociatedObject(self,?&kRoundingCorners);
[self?zy_cornerRadiusWithImage:self.image?cornerRadius:radius.floatValue?rectCornerType:roundingCorners.unsignedLongValue];
}
這樣不需要離屏渲染的UIImageView圓角工具ZYCornerRadius就完成了,有問(wèn)題或建議歡迎發(fā)issues交流,還希望大家star以支持啊,謝謝!
Usage:ZYCornerRadius提供兩種使用方式
Category方式:
導(dǎo)入頭文件
1
#import?"UIImageView+CornerRadius.h"
創(chuàng)建圓角半徑為6的UIImageView(三種方式):
1
2
3
4
5
6
7
8
9
10
11
12//1
UIImageView?*imageView?=?[UIImageView?zy_cornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
//2
UIImageView?*imageView?=?[[UIImageView?alloc]?initWithCornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
//3
UIImageView?*imageView?=?[[UIImageView?alloc]?init];
[imageView?zy_cornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
創(chuàng)建圓形的UIImageView(三種方式):
1
2
3
4
5
6
7
8
9
10
11
12//1
UIImageView?*imageView?=?[UIImageView?zy_roundingRectImageView];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
//2
UIImageView?*imageView?=?[[UIImageView?alloc]?initWithRoundingRectImageView];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
//3
UIImageView?*imageView?=?[[UIImageView?alloc]?init];
[imageView?zy_cornerRadiusRoundingRect];
imageView.image?=?[UIImage?imageNamed:@"mac_dog"];
子類ZYImageView方式同理:
導(dǎo)入頭文件
1
#import?"ZYImageView.h"
使用方式同理
以下列出ZYCornerRadius所開(kāi)放的主要的func:
配置一個(gè)圓角UIImageView,傳入圓角半徑和圓角類型
1
2+?(UIImageView?*)zy_cornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;
-?(instancetype)initWithCornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;
配置一個(gè)圓形的UIImageView
1
2+?(UIImageView?*)zy_roundingRectImageView;
-?(instancetype)initWithRoundingRectImageView;
直接為UIImageView設(shè)置圓角圖片,傳入U(xiǎn)IImage,圓角半徑和圓角類型,當(dāng)次有效!
1
-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;
以下記錄失敗過(guò)程...
嘗試在-drawRect中做切角操作
1.內(nèi)存使用過(guò)大,造成更多的性能損耗
嘗試從init出發(fā)
1.需要事先傳入Image,而且當(dāng)Image改變后無(wú)效,不適合實(shí)際生產(chǎn)
嘗試從-layoutSubviews下手
1.在Category中重寫該方法會(huì)造成不可挽回的結(jié)果
在setImage中設(shè)置好標(biāo)識(shí)符開(kāi)關(guān),在layoutSubviews中判斷開(kāi)關(guān)狀態(tài)再執(zhí)行操作
1.雖然解決了對(duì)其他UIImageView的影響,可實(shí)現(xiàn)方式過(guò)于投機(jī)取巧過(guò)于費(fèi)力。
嘗試直接從重寫-setImage下手
1.直接重寫會(huì)導(dǎo)致無(wú)限遞歸
2.自己重寫為UIImageView顯示圖片的機(jī)制,不熟悉源碼實(shí)現(xiàn),擔(dān)心造成什么遺漏。
最壞的打算,大膽使用swizzleMethod。
Relation:@liuzhiyi1992on Github