zoukankan      html  css  js  c++  java
  • 一行代码让圆角风雨无阻,告别离屏渲染性能损耗


    github链接: https://github.com/liuzhiyi1992/ZYCornerRadius
    本人博客地址:http://zyden.vicp.cc/zycornerradius/

    这篇文章介绍ZYCornerRadius(一句代码。圆角风雨无阻)解决生产中圆角带来的离屏渲染问题的思路。

    日常生产中app布局离不开漂亮的圆角(RounderCorner)。特别是用圆角UIImageView来做数据呈现交互,可是这样的柔和易于让人接受的视图效果并不不过改变了一个形状那么简单。须要付出一定的性能代价。

    相信这已经是总所周知的问题了。日常我们使用layer的两个属性。简单的两行代码就能实现圆角的呈现

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

    由于这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作。也就是离屏渲染,这会给我们带来额外的性能损耗。假设这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表如今用户体验上—-掉帧。这也是我亲身体验过的。有一次朋友在玩我手机的时候问我为什么会卡,看了后才发现原来是一个充满圆形头像的TableView。

    屏幕的渲染机制这里就不copy了,非常多朋友的文章也讨论过这样的问题。这篇文章有深入介绍屏幕显示机制。这里顺便贴一下我笔记里记录的会引发离屏渲染的操作。给大家做个记忆捆绑。正确与否大家能够自己思量。
    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.

    由于这些效果均被觉得不能直接呈现于屏幕,而须要在别的地方做额外的处理预合成。详细的检測我们能够使用Instruments的CoreAnimation。


    ZYCornerRadius

    下面介绍ZYCornerRadius(以Category的方式工作)对UIImageView设置圆角会触发离屏渲染的解决思路,有什么问题和建议还请大家发issues指导更正。

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

    既然我们要避免让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在模拟器中打开了Color Blended Layers Debug所看见的差别:

    一些没有被设置为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与原先对照:

    prefect!


    实际生产

    眼下我们攻克了离屏渲染的问题,可这并不符合实际生产。在app中显示的网络图片我们不可能事先知道并且调用- (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:来进行切角,也不可能每次都还要写个SDWedImage的complete回调去做这个操作,我决定用swizzleMethod的办法来处理。关于对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就完毕了,有问题或建议欢迎发issues交流,还希望大家star以支持啊,谢谢!

    issues

    收到一个朋友的issues: (在0.6.1解决)
    当tableViewCell被selected后(去除selected效果则不会)。里面被切角的imageView效果会丢失,触发重用后也不能恢复,除非又一次setImage。

    思考这个问题从这里出发:我们将处理后的图片显示于屏幕之上是通过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 *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"];

    创建圆形的UIImageView(三种方式):

    //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方式同理:
    导入头文件

    #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.6.1'


    最近更新:

    0.6.1 - 解决在TableViewCell被selected后,当中UIImageView的image被重置的问题
    0.5.1 - 解决SDWebImage使用placeholder为nil时发生的crash
    0.4.1 - 公布第一个较完好版本号


    下面记录失败过程。

    。。

    • 尝试在-drawRect中做切角操作
      1.内存使用过大。造成很多其它的性能损耗
    • 尝试从init出发
      1.须要事先传入Image,并且当Image改变后无效,不适合实际生产
    • 尝试从-layoutSubviews下手
      1.在Category中重写该方法会造成不可挽回的结果
    • 在setImage中设置好标识符开关。在layoutSubviews中推断开关状态再运行操作
      1.尽管攻克了对其它UIImageView的影响。可实现方式过于投机取巧过于费力。

    • 尝试直接从重写-setImage下手
      1.直接重写会导致无限递归
      2.自己重写为UIImageView显示图片的机制,不熟悉源代码实现,操心造成什么遗漏。
    • 最坏的打算,大胆使用swizzleMethod。


    Relation:

    @liuzhiyi1992 on Github


    License:

    ZYCornerRadius is released under the MIT license.

  • 相关阅读:
    nginx负载均衡实现
    shiro 退出 清除缓存
    从零到实现Shiro中Authorization和Authentication的缓存
    Mysql 语句
    N! java
    大数java(pow)
    HDU_1548
    Mike and strings 798B
    Array Division 808D
    poj_1979(dfs)
  • 原文地址:https://www.cnblogs.com/mfmdaoyou/p/7137574.html
Copyright © 2011-2022 走看看