55 (OC)* 图片圆角处理

iOS图片设置圆角性能优化

 

问题

圆角虽好,但如果使用不当,它就是你的帧数杀手,特别当它出现在滚动列表的时候。下面来看圆角如何毁掉你的流畅度的。

实测

layer.cornerRadius

我创建了一个简单地UITableView视图,为每个cell添加了2个UIImageView实例,且为UIImageView实例进行如下设置

aImageView.layer.cornerRadius = aImageView.frame.size.width/2.0;

aImageView.layer.masksToBounds = YES;

运行截图如下:


 

你们猜,现在滚动的帧率是多少。


 

已经跌至45帧每秒,这个帧率已经让人感觉到不那么顺滑了,如果低于40帧每秒,普通用户就会察觉明显的不流畅了。当我把cell的UIImageView实例增加至四个


 

现在帧率已经低于30帧每秒了


 

这个帧率如果出现在首屏,足以引领你的app进入垃圾级别的体验了。 现在我把UIImageView实例的size调的小一些。


 

平均帧率提高了大概3帧每秒。


 

在这里视图和圆角的大小对帧率并没有什么卵影响,数量才是伤害的核心输出啊。

原理

上面拖慢帧率的原因其实都是Off-Screen Rendering(离屏渲染)的原因。离屏渲染是个好东西,但是频繁发生离屏渲染是非常耗时的。

 

Off-Screen Rendering

离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么卵影响,数量才是伤害的核心输出啊。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换

 

上下文切换

上下文切换,不管是在GPU渲染过程中,还是一直所熟悉的进程切换,上下文切换在哪里都是一个相当耗时的操作。首先我要保存当前屏幕渲染环境,然后切换到一

个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen

 Rendering或者再开始一个新的离屏渲染重复之前的操作。 下图描述了一次mask的渲染操作。

 


 

 

一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若

 再加上下文环境切换,一次mask就是普通渲染的30倍以上耗时操作。问我这个30倍以上这个数据怎么的出来的?当我在cell的UIImageView

 的实例增加到150个,并去掉圆角的时候,帧数才跌至28帧每秒。虽然不是甚准确,但至少反映mask这个耗时是无mask操作的耗时的数十倍的。

 

第一种:设置CALayer的cornerRadius

 imageView.image = [UIImage imageNamed:@"img"];
 imageView.image.layer.cornerRadius = 5;
 imageView.image.layer.masksToBounds = YES;

这样设置会触发离屏渲染,比较消耗性能。比如当一个页面上有十几头像这样设置了圆角

会明显感觉到卡顿。
这种就是最常用的,也是最耗性能的。

注意:ios9.0之后对UIImageView的圆角设置做了优化,UIImageView这样设置圆角
不会触发离屏渲染,ios9.0之前还是会触发离屏渲染。而UIButton还是都会触发离屏渲染。

第二种

imageView.clipsToBounds = YES;
imageView.layer setCornerRadius:50];
imageView.layer.shouldRasterize = YES;

shouldRasterize=YES设置光栅化,可以使离屏渲染的结果缓存到内存中存为位图, 使用的时候直接使用缓存,节省了一直离屏渲染损耗的性能。

但是如果layer及sublayers常常改变的话,它就会一直不停的渲染及删除缓存重新 创建缓存,所以这种情况下建议不要使用光栅化,这样也是比较损耗性能的。

第三种 :通过Core Graphics重新绘制带圆角的视图

这种方式性能最好,但是UIButton上不知道怎么绘制,可以用UIimageView添加个 点击手势当做UIButton使用

@implementation UIImage (CircleImage)

- (UIImage *)drawCircleImage {
   UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale); 
   [[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:50] addClip]; 
   [self drawInRect:self.bounds]; 

UIImage *output = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();
   return output; 
}
@end
//在需要圆角时调用如下
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    UIImage *img = [[UIImage imageNamed:@"image.png"] drawCircleImage];
    dispatch_async(dispatch_get_main_queue(), ^{
        imageView.image = img;
    });
});

四、通过混合图层

此方法就是在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。图层混合的透明度处理方式与mask正好相反。此方法虽然是最优解,没有离屏渲染,没有额外的CPU计算,但是应用范围有限。


总结

  1. 在可以使用混合图层遮挡的场景下,优先使用第四种方法。
  2. 即使是非iOS9以上系统,第一种方法在综合性能上依然强于后两者,iOS9以上由于没有了离屏渲染更是首选。
  3. 方法三由于需要大量计算和增加部分内存,需要实际情况各自取舍。

对图片进行圆角处理会相比于直角,它更加柔和优美,是一种很常见的视图效果,在APP中常用于对用户头像的美化,但是设置不当就会让你的APP性能下降,导致掉帧,影响用户体验

首先介绍一下常用的切圆角的几种常见方式

第一种方法:通过设置layer的属性

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; //只需要设置layer层的两个属性  
//设置圆角    
 imageView.layer.cornerRadius = imageView.frame.size.width / 2; 
//将多余的部分切掉
 imageView.layer.masksToBounds = YES;
 [self.view addSubview:imageView];

但是这种方式切圆角的很大一个弊端就是影响性能,对于图片较少的情况下还不是很明显,不会太影响帧数,但是图片多的时候就会严重影响帧数(视图和圆角的大小对帧率并没有什么卵影响,数量才是伤害的核心输出),大概圆角的数量在30-40左右的时候,界面就会卡出翔的节奏,所以开发中一般不会用这个方法.
一开始我以为导致帧数下降的原因是imageView.layer.cornerRadius 这个方法造成的,后来才发现罪魁祸首是imageView.layer.masksToBounds ,这玩意会导致离屏渲染,
离屏渲染我并没有深入的去了解,在网上找资料大体的了解下

渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表现在用户体验上----掉帧

但是ios9之后苹果对离屏渲染做了优化
1.iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染
2.iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
这可能是苹果也意识到离屏渲染会产生性能问题,所以能不产生离屏渲染的地方苹果也就不用离屏渲染了。
对第一种方法的总结
1.如果图片数量很少,能够只用 cornerRadius 解决问题,就不用优化。
2.如果必须设置 masksToBounds,而且只是设置UIImageView的圆角,那么久不用考虑离屏渲染,如果设置其他的空间,那么就要在空间设置圆角过多的情况下舍弃这一种方法

第二种方法:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; 
imageView.image = [UIImage imageNamed:@"1"]; 
//开始对imageView进行画图 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0); 
//使用贝塞尔曲线画出一个圆形图 
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
 [imageView drawRect:imageView.bounds];
 imageView.image = UIGraphicsGetImageFromCurrentImageContext(); 
//结束画图 
UIGraphicsEndImageContext(); [self.view addSubview:imageView];

第三种方法:使用CAShapeLayer和UIBezierPath设置圆角

#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad { [super viewDidLoad]; 
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"1"]; 
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
 CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; 
//设置大小 
maskLayer.frame = imageView.bounds; 
//设置图形样子 
maskLayer.path = maskPath.CGPath;
 imageView.layer.mask = maskLayer; 
[self.view addSubview:imageView];}

1.首先是CAShapeLayer
1.1CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
1.2CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
1.3使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
1.4CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况
总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用第三种

框架ZYCornerRadius

另外在给大家介绍一个第三方框架,虽然star不多,但是感觉挺使用,个人使用的不是太多,因为平时工作中大量的图片切圆角并没有遇到,
地址:https://github.com/liuzhiyi1992/ZYCornerRadius
我简单的了解了下,作者用了两种途径去实现,一个是uiimageView的分类,另外一个是子类实现,而且用到了runtime运行时的知识,(给分类中增加私有属性),总体会大大降低了设置圆角是产生的内存占用高和帧数低的问题,而且还支持多种带边框的圆角,这里就不在多赘述了,大家有需要的可以去github去克隆下来研究研究新的url。

最后总结

1.对于圆角少的情况下,而且是ios9以上,设置图片可以不用考虑离屏渲染,数量少的阴影和其他控件的圆角设置也影响不大
2.数量多的情况,而且ios9以下的情况切忌使用cornerRadius,maskToBounds来设置,掉帧太严重,尽量使用贝塞尔曲线和Core Graphics框架或者CAShapeLayer来去实现,或者用第三方框架

posted on 2019-08-24 11:17  风zk  阅读(421)  评论(0编辑  收藏  举报

导航