iOS-高效设置圆角

作者: doudo | 来源:发表于2017-08-24 14:24 被阅读1280次

    一、前因

    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 属性来绘制圆角:

    1. UIView的contents无内容可以直接通过设置cornerRadius达到效果。
    2. 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的离屏渲染。
    方案:放到后台线程中进行。

    参考资料:
    iOS 高效添加圆角效果实战讲解
    iOS 离屏渲染优化(Offscreen Render)

    相关文章

      网友评论

        本文标题:iOS-高效设置圆角

        本文链接:https://www.haomeiwen.com/subject/exwsdxtx.html