美文网首页iOS开发那些事
iOS圆角图片的实现

iOS圆角图片的实现

作者: 雷鸣1010 | 来源:发表于2017-02-16 11:58 被阅读165次

    demo地址

    一:相对简便的圆角图片的实现方式

    imageView.layer.cornerRadius = CGFloat(10);
    imageView.layer.masksToBounds = YES;
    
    

    由于这样的处理机制是GPU在当前缓冲区以外新开辟一个渲染缓冲区进行工作,也就是我们常说的离屏渲染;
    这会给我们带来额外的性能消耗,如果这样的圆角达到一定数量,会触发渲染缓冲区的频烦合并和上下文的频繁切换,性能的代价会很明显的表现在用户体验上,因为这些效果均被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。具体的检测我们可以使用Instruments的CoreAnimation

    二: 使用ZYCornerRadius解决设置圆角时GPU会触发离屏渲染的解决思路的问题

    先上一张性能对比图
    测试设备6P,屏幕中有40张尺寸为20*20的小图片,使用masksToBounds切角处理时帧率大大下降至20+,使用ZYCornerRadius时帧率保持在57+,性能接近0损耗

    ZYCornerRadius性能对比.png

    另外内存使用的对比(使用ZYCornerRadius处理大量的圆角图片几乎没有带来内存增长):

    内存使用对比.jpg

    既然我们要避免让GPU触发离屏,那么只能把兵符交给CPU,虽然CPU对图形的处理能力不及GPU,但由于这种处理的难度不大,且代价肯定远小于上下文切换。
    其实一开始的想法就是从-drawRect下手,但是看了某篇文章(找不回来了)后打消了这个念头,-drawRect的确存在很多性能坑。
    思路
    既然不能让控件masksToBounds,ZYCornerRadius就从图片本身下手,我使用在UIKit中对Core Graphics有一定封装的应用层类UIBezierPath,对图片进行破坏性的切角,破坏性仅仅是对切去部分而言,当然这操作是在CPU内完成的,而后我只需要取到处理完成的bitmap(可为UIImage对象)交给GPU显示于屏幕即可。

    /**
     * @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);
    
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            UIGraphicsBeginImageContextWithOptions(size, YES, scale);
            if (nil == UIGraphicsGetCurrentContext()) {
                return;
            }
            UIBezierPath *cornerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCornerType cornerRadii:cornerRadii];
            [cornerPath addClip];
            [image drawInRect:self.bounds];
            id processedImageRef = (__bridge id _Nullable)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
            UIGraphicsEndImageContext();
    
            dispatch_async(dispatch_get_main_queue(), ^{
                self.layer.contents = processedImageRef;
            });
        });
    }
    

    可见,我对图片进行了切角处理后,将得到的含圆角UIImage通过-setImage传给了UIImageView。操作没有触发GPU离屏渲染,过程在CPU内完成,而后我在Demo中证实了这个方法。

    顺便一提这里还存在一个性能问题,==Color Blended Layers==,UIGraphicsBeginImageContextWithOptions(<#CGSize size#>, <#BOOL opaque#>, <#CGFloat scale#>)的第二个参数是透明通道的开关,true则为不透明。以下两张图是参数传NO or YES在模拟器中

    左边图片传入的参数为YES,右边的图片传入的参数为NO

    组合演示.jpg

    打开打开了Color Blended Layers Debug所看见的区别:

    屏幕快照 2017-02-16 下午2.19.00.png

    一些没有被设置为opacity的图层,因为透明通道的存在,系统需要去计算图层堆叠后像素点的真实颜色,在Instruments的测试中也是可以高亮标显出来,这种性能的损耗程度我还没有专门去测试。但是在上图可以看见如果设置为不包含透明通道,我们图片被剪去的部分就没有了颜色(黑漆漆一片),这里使用的解决方案就是在图片上下文中先画一层backgroundColor,缺点就是需要传入:

    /**
     * @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);
    
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            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];
            id processedImageRef = (__bridge id _Nullable)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
            UIGraphicsEndImageContext();
    
            dispatch_async(dispatch_get_main_queue(), ^{
                self.layer.contents = processedImageRef;
            });
        });
    }
    

    传入红色的背景颜色,打开Color Blended Layers Debug与原先对比:

    组合演示2.png

    实际生产

    目前我们解决了离屏渲染的问题,可这并不符合实际生产,在app中显示的网络图片我们不可能事先知道并且调用

    • (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:

    来进行切角,也不可能每次都还要写个SDWedImage的complete回调去做这个操作,我决定用swizzleMethod的办法来处理

    我们把对self.image切角处理放在每次layoutSubviews的时候完成,大家看到这里顿时把我臭骂了一顿。。。在Category里重写-layoutSubviews的致命的,这的确会导致整个项目下所有的UIImageView都会去执行这个山寨的-layoutSubviews,别慌关掉文章,给个机会继续看下去。

    首先我们需要将使用者传入的切角参数保存起来,供-layoutSubviews切角时使用,因为category不支持扩展属性,所以我们可以用runtime来做:

    /**
     * @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)];
    }
    
    

    细心的朋友可以看见上面这段代码里的+swizzleMethod,我将调用了- (void)zy_cornerRadiusAdvance:cornerRadius:rectCornerType:的UIImageView对象的-layoutSubviews方法的实现转移到了我自己的方法-zy_LayoutSubviews上,也就是说我不需要去重写-layoutSubviews,而主动调用过-zy_cornerRadiusAdvance的UIImageView对象的-layoutSubviews的实现却被我换成了-zy_LayoutSubviews,源代码在Demo中有。ok,于是在-zy_LayoutSubviews中收官:

    - (void)zy_LayoutSubviews {
        [super layoutSubviews];
        NSNumber *radius = objc_getAssociatedObject(self, &kRadius);
        NSNumber *roundingCorners = objc_getAssociatedObject(self, &kRoundingCorners);
    
        [self zy_cornerRadiusWithImage:self.image cornerRadius:radius.floatValue rectCornerType:roundingCorners.unsignedLongValue];
    }
    

    同时结合KVO维持切角效果做持久化:
    UIImageView的内容image可能会因为许多动作而导致被重新设置,我们的切角效果就会因此而消失掉,我们需要对image属性进行监听,一旦发生变化即对改变后的新值再次作切角处理,再次赋值给imageView.layer.content显示。KVO响应部分源码:

    
    #pragma mark - KVO for .image
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if ([keyPath isEqualToString:@"image"]) {
            UIImage *newImage = change[NSKeyValueChangeNewKey];
            if ([newImage isMemberOfClass:[NSNull class]]) {
                return;
            } else if ([objc_getAssociatedObject(newImage, &kZYProcessedImage) intValue] == 1) {
                return;
            }
            if (_isRounding) {
                [self zy_cornerRadiusWithImage:newImage cornerRadius:self.frame.size.width/2 rectCornerType:UIRectCornerAllCorners];
            } else if (0 != _cornerRadius && _rectCornerType && nil != self.image) {
                [self zy_cornerRadiusWithImage:newImage cornerRadius:_cornerRadius rectCornerType:_rectCornerType];
            }
        }
    }
    

    这样不需要离屏渲染的UIImageView圆角工具ZYCornerRadius就完成了
    思考这个问题从这里出发:我们将处理后的图片显示于屏幕之上是通过KVO对imageView.image的监听,再将处理后的图片上下文赋值给imageView的mainLayer,这个过程完成后其实imageView持有的image就没有被玷污的,同时_image也是,当tableViewCell被selected后触发了subViews的重绘后,小三还是会被正室所取代。

    解决这个问题:
    回到过去,使用setImage让处理后的图片显示于屏幕,使用runtime对image对象绑定一个标识符,因KVO存在的无限递归。

    ** Usage(一句代码,圆角风雨无阻)**

    ZYCornerRadius提供两种使用方式

    Category方式:
    导入头文件

    #import "UIImageView+CornerRadius.h"
    

    创建圆角半径为6的UIImageView(三种方式):

    //1
        UIImageView *imageView1 = [[UIImageView alloc]init];
      [imageView1 zy_cornerRadiusAdvance:75.0f rectCornerType:UIRectCornerAllCorners];
        [imageView1 setFrame:CGRectMake(130, 80, 150, 150)];
    
        imageView1.image = [UIImage imageNamed:@"mac_dog"];
        
        [self.view addSubview:imageView1];
    

    子类ZYImageView方式同理:
    导入头文件

    #import "ZYImageView.h"
    

    使用方式同理

    以下列出ZYCornerRadius所开放的主要的func:

    配置一个圆角UIImageView,传入圆角半径和圆角类型

    + (UIImageView *)zy_cornerRadiusAdvance:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType;
    - (instancetype)initWithCornerRadiusAdvance:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType;
    

    配置一个圆形的UIImageView

     (UIImageView *)zy_roundingRectImageView;
    - (instancetype)initWithRoundingRectImageView;
    
    

    直接为UIImageView设置圆角图片,传入UIImage,圆角半径和圆角类型,当次有效!

    - (void)zy_cornerRadiusWithImage:(UIImage *)image cornerRadius:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType;
    

    CocoaPods:

    pod 'ZYCornerRadius', '~> 0.8.1'
    

    以下记录失败过程。。。

    尝试在-drawRect中做切角操作
    1.内存使用过大,造成更多的性能损耗
    尝试从init出发
    1.需要事先传入Image,而且当Image改变后无效,不适合实际生产
    尝试从-layoutSubviews下手
    1.在Category中重写该方法会造成不可挽回的结果
    在setImage中设置好标识符开关,在layoutSubviews中判断开关状态再执行操作
    1.虽然解决了对其他UIImageView的影响,可实现方式过于投机取巧过于费力。
    尝试直接从重写-setImage下手
    1.直接重写会导致无限递归
    2.自己重写为UIImageView显示图片的机制,不熟悉源码实现,担心造成什么遗漏。
    最坏的打算,大胆使用swizzleMethod。

    转发:http://www.jianshu.com/p/2fbb4c8fb1fa

    绘图iOS Quarat2D http://www.jianshu.com/p/8cf8d4b724d2

    相关文章

      网友评论

        本文标题:iOS圆角图片的实现

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