美文网首页iOS之性能优化16iOS高阶UI相关
如何优雅的为UIView添加圆角?

如何优雅的为UIView添加圆角?

作者: 思考的快与慢 | 来源:发表于2016-11-23 10:52 被阅读1121次

    一.在解决UIView的圆角问题之前,我们先看一下UITableView的卡顿问题。

    1.在解决关于添加圆角的问题之前,大家先看一下这个页面。下面的截图是我之前做的项目中的一个页面。大家可以看到这个页面中有大量的圆角,包括头像圆角,view圆角等,另外这个TableViewCell的高度是不确定的。在开发这个页面的时候,整体采用的是自动布局的方法,页面在滚动的过程中很是卡顿,下面是对这个页面一步一步的优化方法。


    WechatIMG2.jpeg
    • 关于TableView的卡顿,无外乎是CPU或者GPU导致的,首先查看代码看有没有在cell的reload方法中做一些复杂的操作,果然犯了一个很SB的错误,在cell的reload方法中,做了一个这个的操作,这个方法会多次的调用,多多次为ImageView添加手势,会造成太多cpu去处理手势的相应。解决方法,把这段代码挪到其他地方。
    @weakify
    [self.askerAvaterImageView bk_whenTap:^{
           @strongify(self)
    }];
    
    • 通过上面的修改之后,发现页面还是不流畅,然后用Instrument工具跑了一下,查找发现高度计算,存在一定的耗时,考虑可能是高度引起的,通过检查代码,发现高度计算我们使用的是
     UITableView+FDTemplateLayoutCell 这个框架中的这个方法,  
    return [tableView fd_heightForCellWithIdentifier:[SNSelectedTableViewCell identifier] configuration:^(SNSelectedTableViewCell *cell) {
        }];
    

    跳到头文件发现在这个类里面有一个构造方法。
    This method does what "-fd_heightForCellWithIdentifier:configuration" does, and calculated height will be cached by its index path, returns a cached height when needed. Therefore lots of extra height calculations could be saved.

    -(CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration;
    

    这个方法意思是用indexPath作为缓存。通过替换方法发现,在第一滑动的时候和刚才差不多,但是在往回滚动的时候,比刚才好多了,由于高度被缓存 了,所以不用再重新计算高度。这样就好多了。

    • 在修改了计算高度的方法之后,其实还不是很放心,就干脆把高度写死了,然后发现最后的效果和使用FDTemplateLayoutCell 在往回滚动的卡顿成都差不多。

    二、关于圆角的绘制

    1.上面主要解决的是cpu导致的卡顿问题,但是并没有太大的改善,大家可能注意到,在这个页面中有好多的圆角头像,考虑到可能是圆角导致的,所以把加载圆角头像的collectionView先移除。发现此时的页面已经非常流畅了,这时再次把问题定位到多个圆角的绘制上。通过instruments 中的timeprofiler 发现drawRecr方法耗时比较多。然后就把问题再一次定位到圆角的绘制上。查看代码,是这样写的。

    self.askerAvatarImageV.layer.cornerRadius = 10.f;
    self.askerAvatarImageV.layer.masksToBounds = YES;
    

    这样写的坏处,不用我多说大家都知道,一是耗时,二是会造成离屏渲染问题。因此把绘制圆角的方法改成下面的方法进行手动绘制。

    - (UIImage*)cirleImage
    {
     // NO代表透明
     UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
    
     // 获得上下文
     CGContextRef ctx = UIGraphicsGetCurrentContext();
     // 添加一个圆
     CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
     CGContextAddEllipseInRect(ctx, rect);
     // 裁剪
     CGContextClip(ctx);
     // 将图片画上去
     [self drawInRect:rect];
     UIImage *cirleImage = UIGraphicsGetImageFromCurrentImageContext();
     UIGraphicsEndImageContext();
     return cirleImage;
    }
    改成这种写法之后,页面再次滚动,比刚才好的太多了。而且离屏渲染的问题也没有了。
    

    2.其实到这只算是我们的第一步优化。通常我们使用SDWebImag的时候,我们知道由于SD的缓存机制,图片下载之后会被缓存到磁盘和内存中一份,所以当我们再去取的时候,就直接从磁盘或者内存中操作,速度比较快,因此就萌生了为什么不能给圆角头像添加缓存的功能呢,所干就干,下面的代码就产生了。

    - (void)setRoundImageWithURL:(NSURL *)url placeHoder:(UIImage *)placeHoder cornerRadius:(CGFloat)radius
    {
        NSString *urlStirng = [url absoluteString];
        NSString *imagenName = [NSString stringWithFormat:@"%@%@",urlStirng,@"roundImage"];
        __block NSString *path = [self base64String:imagenName];
        UIImage *image = [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:path];
        if (image)
        {
            [self setImage:image];
            return;
        }
        else if (placeHoder)
        {
            [self setImage:placeHoder];
        }
        __weak typeof(self)weakSelf = self;
        if (url)
        {
            [[SDWebImageManager sharedManager] downloadImageWithURL:url options:SDWebImageRetryFailed|SDWebImageLowPriority progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                __strong typeof(weakSelf)stongSelf = weakSelf;
                if (!finished) return;
                if (!image) return;
                @synchronized(self) {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                        UIImage *cirleImage =  [image cirleImage];
                        [[SDImageCache sharedImageCache]storeImage:cirleImage forKey:path];
                        dispatch_async(dispatch_get_main_queue(), ^{
                            [stongSelf performSelector:@selector(reloadImageData:) withObject:cirleImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
                        });
                    });
                }
            }];
        }
    }
    
    - (NSString*)base64String:(NSString*)str
    {
        NSData* originData = [str dataUsingEncoding:NSASCIIStringEncoding];
        NSString* encodeResult = [originData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
        return [NSString stringWithFormat:@"%@.png",encodeResult];
    }
    
    - (void)reloadImageData:(UIImage*)image
    {
        [self setImage:image];
    }
    

    在这端代码中[image cirleImage] 这个方法就是为UIImage写的一个分类就是上面用来处理圆角的方法。
    我们把加载头像的方法替换成这个方法。瞬间感觉又流畅了好多。嘚瑟一下。

    3.对于通过这种方法处理后的结果自己还是挺满意的,但是同事突然间来了一句你为什么不能通过一个空心的矩形盖在上面呢。这样就不用处理圆角了。我去,他说的貌似很有道理的样子,我竟无言以对。刚开始想着是要不让UI给出一张这样的空心矩形,但是想了想,用到头像的地方比较多,每个地方都要切,多麻烦UI妹子,我多心疼啊。然后就自己想着自己画一个吧,说搞就搞,刚开始没有思路,网上也没有这样的方法,然后再stackoverflow上面无意间看到了这样一个问题,然后把上面贴的代码复制粘贴一发。结果一运行,发现不好使,顿时很是失望。没办法。自己改,经过我的不懈努力。总算是显示对了,绘制空心矩形的代码如下:

    - (void)drawRect:(CGRect)rect
    {
     [super drawRect:rect];
     CAShapeLayer *maskLayer = [CAShapeLayer layer];
     maskLayer.frame = self.bounds;
     // 画圆
     CGFloat radius = self.bounds.size.width / 2;
     CGRect tempRect = CGRectMake(CGRectGetMidX(self.frame) - radius, CGRectGetMidY(self.frame) - radius, 2 * radius,2 * radius);
     UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
     maskLayer.fillRule = kCAFillRuleEvenOdd;
     maskLayer.fillColor = [UIColor whiteColor].CGColor;
     [path appendPath:[UIBezierPath bezierPathWithOvalInRect:tempRect]];
     maskLayer.path = path.CGPath;
     self.layer.mask = maskLayer;
    }
    

    4.完成上面的空心矩形的绘制,其实绘制空心圆角的已经完成一半了,完整的代码如下,稍后会把代码放到github上。主要是给UIView加了一个分类。

    头文件如下:
    @interface UIView (SNRoundCorner)
    /**
     *  圆角的颜色-这个颜色需要和背景颜色设置一致
     */
    @property (nonatomic, strong)  UIColor *sn_roundCornerColor;
    /**
     *  圆角的半径
     */
    @property (nonatomic, assign)  CGFloat sn_roundCornerRadius;
    @end
    
    .m文件实现如下
    @interface UIView()
    
    @property (nonatomic, strong) CALayer *sn_maskLayer;
    
    @end
    
    @implementation UIView (SNRoundCorner)
    
    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            SEL selectors[] = {
                @selector(layoutSubviews),
                @selector(setFrame:)
            };
            
            for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
                SEL originalSelector = selectors[index];
                SEL swizzledSelector = NSSelectorFromString([@"sn_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
                
                Method originalMethod = class_getInstanceMethod(self, originalSelector);
                Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
                
                BOOL addedSuccess = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
                if (addedSuccess)
                {
                    class_replaceMethod(self, originalSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
                }
                else
                {
                    method_exchangeImplementations(originalMethod, swizzledMethod);
                }
            }
        });
    }
    
    - (void)sn_layoutSubviews
    {
        if (self.sn_roundCornerColor && self.sn_roundCornerRadius > 0 && !self.sn_maskLayer)
        {
            [self addRoundCorner];
        }
        [self sn_layoutSubviews];
    }
    
    - (void)sn_setFrame:(CGRect)frame
    {
        [self sn_setFrame:frame];
        if (self.sn_maskLayer)
        {
            if (!CGSizeEqualToSize(frame.size, self.sn_maskLayer.frame.size))
            {
                [self.sn_maskLayer removeFromSuperlayer];
                self.sn_maskLayer = nil;
            }
        }
    }
    
    #pragma mark - 添加分类属性
    - (UIColor *)sn_roundCornerColor
    {
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setSn_roundCornerColor:(UIColor *)sn_roundCornerColor
    {
        objc_setAssociatedObject(self, @selector(sn_roundCornerColor), sn_roundCornerColor, OBJC_ASSOCIATION_RETAIN);
    }
    
    - (CGFloat)sn_roundCornerRadius
    {
        return [objc_getAssociatedObject(self, _cmd) floatValue];
    }
    
    - (void)setSn_roundCornerRadius:(CGFloat)sn_roundCornerRadius
    {
        objc_setAssociatedObject(self, @selector(sn_roundCornerRadius), @(sn_roundCornerRadius), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (CALayer *)sn_maskLayer
    {
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setSn_maskLayer:(CALayer *)sn_maskLayer
    {
        objc_setAssociatedObject(self, @selector(sn_maskLayer), sn_maskLayer, OBJC_ASSOCIATION_RETAIN);
    }
    
    /**
     *  根据当前图片当前返回一个经过处理之后的圆角图片
     *  @return UIImage
     */
    - (void)addRoundCorner
    {
        // 这段代码的作用是保证只添加一次
        if (self.sn_maskLayer)
        {
            NSString *reason = @"这个属性只允许设置一次,再次设置不会生效";
            @throw [NSException exceptionWithName:NSGenericException
                                           reason:reason
                                         userInfo:nil];
        }
        else
        {
            [self.layer addSublayer:[self createMaskImageView]];
        }
    }
    
    - (CALayer *)createMaskImageView
    {
    
        NSString *imageCacheKey = [NSString stringWithFormat:@"%@%@%@",@"snob_mass_roundImage",NSStringFromCGSize(self.bounds.size),[self rgbStringWithColor:self.sn_roundCornerColor]];
        UIImage *image = [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:imageCacheKey];
        if (image)
        {
            self.sn_maskLayer = [CALayer layer];
            self.sn_maskLayer.frame = self.bounds;
            // 解决离屏渲染问题
            self.sn_maskLayer.shouldRasterize = YES;
            self.sn_maskLayer.rasterizationScale = [UIScreen mainScreen].scale;
            self.sn_maskLayer.contents = (id)image.CGImage;
            return self.sn_maskLayer;
        }
        else
        {
            CALayer *roundLayer = [CALayer layer];
            roundLayer.frame = self.bounds;
            roundLayer.backgroundColor = self.sn_roundCornerColor.CGColor;
            CAShapeLayer *maskLayer = [CAShapeLayer layer];
            maskLayer.frame = self.bounds;
            
            // 画圆
            UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
            maskLayer.fillRule =  kCAFillRuleEvenOdd;
            [path appendPath:[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.sn_roundCornerRadius]];
            maskLayer.path = path.CGPath;
            roundLayer.mask = maskLayer;
            
            CGSize size = roundLayer.bounds.size;
            // 下面方法,第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了
            UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
            [roundLayer renderInContext:UIGraphicsGetCurrentContext()];
            UIImage *tempImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            
            // 缓存图片
            [[SDImageCache sharedImageCache] storeImage:tempImage forKey:imageCacheKey toDisk:NO];
            self.sn_maskLayer = roundLayer;
            return self.sn_maskLayer;
        }
    }
    
    #pragma mark - 获取当前颜色的rgb值
    - (NSString *)rgbStringWithColor:(UIColor *)color
    {
        CGColorRef colorRef = color.CGColor;
        NSInteger numComponents = CGColorGetNumberOfComponents(colorRef);
        NSMutableString *stringM = [NSMutableString string];
        for (int i = 0; i < numComponents; i++)
        {
            const CGFloat *components = CGColorGetComponents(colorRef);
            [stringM appendString:@(components[i]).stringValue];
        }
        return [stringM copy];
    }
    
    

    5.需要重点说明的是,目前github上有很多解决圆角的代码,但是都不支持自动布局,所有我才觉得写这个代码很重要。今天就写到这吧,这是我的第一篇简书博客,后续会继续……

    放出地址 (https://github.com/zwcshy/SNRoundCorner.git)

    相关文章

      网友评论

      • 神采飞扬_2015:这个方法有一个问题,就是点击cell选中时,圆角头像的背景还是方框。
      • Laugamjeon:博主,对于你那里的二.3的CAShapeLayer+Bezier绘制那一部分,你有没有用过instruments去测过,我感觉是不太好使的,这个方法会导致离屏渲染
        克理斯:@啊了个超 那是因为里边没有子view
        思考的快与慢:你可以下载一下我的代码看一下,我自己测试的是没有离屏渲染的问题了
      • 男人宫:兄弟,UITableView+FDTemplateLayoutCell这个框架在10.2的系统中不会蹦吗
        思考的快与慢:不会蹦,是不是你的约束使用的不对。
      • LiYaoPeng:很不错,受教了~
        思考的快与慢:@LiYaoPeng 很少写,试着写写。

      本文标题:如何优雅的为UIView添加圆角?

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