一行代码,圆角风雨无阻,告别离屏渲染性能损耗-程序员宅基地

技术标签: cocoapods  runtime  

这篇文章介绍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损耗。 

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

既然我们要避免让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(三种方式):

//1UIImageView *imageView = [UIImageView zy_cornerRadiusAdvance:6.0f rectCornerType:UIRectCornerAllCorners];  
imageView.image = [UIImage imageNamed:@"mac_dog"];//2UIImageView *imageView = [[UIImageView alloc] initWithCornerRadiusAdvance:6.0f rectCornerType:UIRectCornerAllCorners];  
imageView.image = [UIImage imageNamed:@"mac_dog"];//3UIImageView *imageView = [[UIImageView alloc] init];  
[imageView zy_cornerRadiusAdvance:6.0f rectCornerType:UIRectCornerAllCorners];
imageView.image = [UIImage imageNamed:@"mac_dog"];

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

//1UIImageView *imageView = [UIImageView zy_roundingRectImageView];  
imageView.image = [UIImage imageNamed:@"mac_dog"];//2UIImageView *imageView = [[UIImageView alloc] initWithRoundingRectImageView];  
imageView.image = [UIImage imageNamed:@"mac_dog"];//3UIImageView *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'



附带一个25行500个圆角的UITabelView性能测试gif

原文地址:http://zyden.vicp.cc/zycornerradius/ 

 

转载于:https://www.cnblogs.com/520Code/p/6861053.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/dianshan3580/article/details/101750044

智能推荐

基于QT4的TCP/UDP客户端程序设计_qt4 tcp-程序员宅基地

文章浏览阅读851次。文章转自: http://blog.sina.com.cn/s/blog_705adafb0101g00d.html设计一个基于QT的客户端程序,该程序使用tcp和udp与服务器端通讯,应用层协议为iec103协议,客户端与服务器端建立tcp连接,交互通讯流程采用tcp方式,定时报文传输采用udp方式,本客户端处理tcp和udp通讯,对应用层数据报文解析后提供回调接口供应用使用,并提供接口供应用..._qt4 tcp

maven项目:重新编译生成class文件_maven重新编译-程序员宅基地

文章浏览阅读1.3w次。原因:由于误删或更新了内容,且不能自动编译时,手动调节。解决步骤:手动clean-maven项目:项目==》右键:run as ==》maven clean 手动输入clean-maven命令:项目==》右键:run as ==》maven build ==》Goals:clean intall package ==》Apply Runclass文件重新生成:OK!..._maven重新编译

前端面试题集锦(一)-程序员宅基地

文章浏览阅读160次。温馨提示:经常看一些面试题,能够很好的对自己进行查缺补漏,检验自己的能力。1、写出transform中2D转换的几大属性及其作用:答:①translate():根据坐标原点,浏览器左上角(0,0)进行X轴、     Y轴上的位置移动    ②rotate():根据X轴进行旋转,正值时为顺时针,负值时为逆时针。(deg单位)    ③scale(parm1,parm2):根据原大小对其进行放大缩小功能,参数一二对应width和height,和X轴、Y轴。    ④skew(parm1,parm

功率的正负,无功,有功功率,容性感性功率_为什么充电功率是负的-程序员宅基地

文章浏览阅读1.8w次,点赞6次,收藏27次。功率当然有正负之分。比如两台并行的发电机发电驱动一台大功率用电设备。如果两台发电机没有协调好的话,很容易负载偏移,就是一台发电机承受负载大,领一台发电机承受负载小,而这种差距是会逐渐变大的,一直变到一台发电机承受所有的负载的同时,还要带另一台发电机转,另一台发电机不承受任何负载,还要被拉着转,这台发电机就属于负功率了。功率有正负之分所谓正负不应以功率消耗为标准,由于功率的定义是单位时间所做_为什么充电功率是负的

echarts图表的使用及用法-程序员宅基地

文章浏览阅读5.2k次。一、介绍ECharts,一个纯 Javascript 的图表库,可以流畅的运行在PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的 Canvas 类库 ZRender,提供直观,生动,可交互,可高度个性化定制的数据可视化图表。ECharts 提供了常规的折线图,柱状图,散点图,饼图,K线图,用于统计的盒形图,用于地理数据可视化的地图,热力图,线图,用于关系数据可视化的关系图,treemap,多维数据可视化的平行坐标,还_echarts图表

容器平台选型的十大模式:Docker、DC/OS、K8S 谁与当先?_开源容器平台-程序员宅基地

文章浏览阅读1.2k次。本文由 网易云 发布作者:刘超 来自:网易云基础服务无论是在社区,还是在同客户交流的过程中,总会被问到到底什么时候该用 Docker?什么时候用虚拟机?如果使用容器,应该使用哪个容器平台? 显而易见,我不会直接给大家一个答案,而是希望从技术角度进行分析具体的场景。例如客户是大公司还是小公司,将部署小集群还是大集群,倾向于私有云还是公有云,已经采购了 IaaS 还是没有 IaaS,IT 运维能力..._开源容器平台

随便推点

springboot gradle 使用过程中遇到的问题小结(6)_android expected a single classpath entry, found: -程序员宅基地

文章浏览阅读194次。1. 报错:Cannot deserialize instance of java.lang.String out of START_OBJECT token,我这里报这个错的原因是传递的json数据类型不对,修正后即可2. 使用JsonFormat引起的时间比正常时间慢8小时,@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GM..._android expected a single classpath entry, found: []

16.忽略大小写的字符串比较_16:忽略大小写的字符串比较-程序员宅基地

文章浏览阅读4.3k次。描述一般我们用strcmp可比较两个字符串的大小,比较方法为对两个字符串从前往后逐个字符相比较(按ASCII码值大小比较),直到出现不同的字符或遇到'\0'为止。如果全部字符都相同,则认为相同;如果出现不相同的字符,则以第一个不相同的字符的比较结果为准(注意:如果某个字符串遇到'\0'而另一个字符串还未遇到'\0',则前者小于后者)。但在有些时候,我们比较字符串的大小时,希望忽略字母的大小_16:忽略大小写的字符串比较

使用labelme制作自己的语义分割数据集_使用labelme训练自己的语义分割流程-程序员宅基地

文章浏览阅读1.4k次,点赞2次,收藏24次。安装labelme在cmd中激活我们使用的python环境,然后使用pip命令安装labelme,命令如下:pip install labelme==3.16.7注意:如果安装最新版本的 labelme,就无须指定版本号(3.16.7就是版本号)打开labelme在cmd中激活我们使用的python环境,然后使用下面的命令,就可以打开labelme软件:labelme界面如下图:使用labelme对数据集进行标注标注之前我们可以先设置一下自动保存:file——>Sav_使用labelme训练自己的语义分割流程

跟随小破站学习java web第十五天_小破站的学习陪伴-程序员宅基地

文章浏览阅读262次。file_upload.jsp<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><h..._小破站的学习陪伴

java解析定长文件入库_java-当直到运行时才知道记录布局时,使用哪种方法来解析具有固定长度记录的文件?...-程序员宅基地

文章浏览阅读392次。我想基于另一个文件中提供的记录布局来解析文件.基本上会有一个定义文件,它是一个用逗号分隔的字段及其各自长度的列表.其中会有很多,每次我运行程序时都会加载一个新的.firstName,text,20middleInitial,text,1lastName,text,20salary,number,10然后,我将显示一个带有提供的列标题的空白表,以及一个通过单击按钮或其他方式添加数据的选项-我尚未决定..._java 文本文件定长记录

week10限时大模拟_B -团队聚餐_s,vb不是在你那边可能在部门聚餐:u女包~、{k:↑二:、:-程序员宅基地

文章浏览阅读178次。B - 团队聚餐题目描述TA团队每周都会有很多任务,有的可以单独完成,有的则需要所有人聚到一起,开过会之后才能去做。但TA团队的每个成员都有各自的事情,找到所有人都有空的时间段并不是一件容易的事情。给出每位助教的各项事情的时间表,你的任务是找出所有可以用来开会的时间段。输入格式第一行一个数T(T≤100),表示数据组数。对于每组数据,第一行一个数m(2 ≤ m ≤ 20),表示TA..._s,vb不是在你那边可能在部门聚餐:u女包~、{k:↑二:、:

推荐文章

热门文章

相关标签