一、前因
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 属性来绘制圆角:
- UIView的contents无内容可以直接通过设置cornerRadius达到效果。
- 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的离屏渲染。
方案:放到后台线程中进行。