iOS的离屏渲染
实际开发中,大多会遇到圆角或者圆形的控件的情况。通常,简便的解决方案主要是:
1.让美工做一个圆角的图片,我们直接放图片就OK。
2.就是常用的layer的那两个属性(cornerRadius , masksToBounds)。
第一种方法不说了,第二种方法,在圆角不多的时候还可以,如果一个界面上的圆角控件很多的时候,再用它就出问题了,。就像下面这种情况的时候,滑动tableView就会明显感觉到屏幕的卡顿:
究其原因,我们在用masksToBounds这个属性的时候GPU屏幕渲染才用了离屏渲染的方式。由此,我们引出了离屏渲染的概念------
离屏渲染是什么?
OpenGL中,GPU屏幕渲染有以下两种方式:
1、On-Screen Rendering:
意思是当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。
2、Off-Screen Rendering:
意思就是我们说的离屏渲染了,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
一、创建新缓冲区,要想进行离屏渲染,首先要创建一个新的缓冲区。
二、上下文切换,离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
会导致离屏渲染的几种情况:
1. custom drawRect: (any, even if you simply fill the background with color)
2. CALayer shadow
3. CALayer mask
4. any custom drawing using CGContext
解决圆角离屏渲染的方案:
圆角使用UIImageView装载一个圆角image来处理,简单地说就是比如一个UIView(或其子类)设置圆角,就在底层铺一个UIImageView,然后用GraphicsContext生成一张带圆角的图片放在UIImageView上。
#import "DrCorner.h"
@interface DrCorner()
@end
@implementation DrCorner
+ (CGFloat)ceilbyunit:(CGFloat)num unit:(double)unit {
return num -modf(num, &unit) + unit;
}
+ (CGFloat)floorbyunit:(CGFloat)num unit:(double)unit {
return num -modf(num, &unit);
}
+ (CGFloat)roundbyunit:(CGFloat)num unit:(double)unit {
CGFloat remain =modf(num, &unit);
if (remain > unit /2.0) {
return [self ceilbyunit:num unit:unit];
}else{
return [self floorbyunit:num unit:unit];
}
}
+ (CGFloat)pixel:(CGFloat)num {
CGFloat unit;
CGFloat scale = [[UIScreen mainScreen] scale];
switch((NSInteger)scale) {
case 1:
unit =1.0/1.0;
break;
case 2:
unit =1.0/2.0;
break;
case 3:
unit =1.0/3.0;
break;
default:
unit =0.0;
break;
}
return [self roundbyunit:num unit:unit];
}
@end
- (void)dr_addCornerRadius:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
backgroundColor:(UIColor*)backgroundColor
borderCorlor:(UIColor*)borderColor {
UIImageView *imageView = [[UIImage Viewalloc] initWithImage:[self dr_drawRectWithRoundedCornerRadius:radius
borderWidth:borderWidth
backgroundColor:backgroundColor
borderCorlor:borderColor]];
[self insertSubview:imageView atIndex:0];
}
- (UIImage*)dr_drawRectWithRoundedCornerRadius:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
backgroundColor:(UIColor*)backgroundColor
borderCorlor:(UIColor*)borderColor {
CGSizesizeToFit =CGSizeMake([DrCorner pixel:self.bounds.size.width],self.bounds.size.height);
CGFloat halfBorderWidth = borderWidth /2.0;
UIGraphicsBeginImageContextWithOptions(sizeToFit,NO, [UIScreen mainScreen].scale);
CGContextRefcontext =UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGFloatwidth = sizeToFit.width, height = sizeToFit.height;
CGContextMoveToPoint(context, width - halfBorderWidth, radius + halfBorderWidth);//准备开始移动坐标
CGContextAddArcToPoint(context, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius);
CGContextAddArcToPoint(context, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius);//左下角角度
CGContextAddArcToPoint(context, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius);//左上角
CGContextAddArcToPoint(context, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius);
CGContextDrawPath(UIGraphicsGetCurrentContext(),kCGPathFillStroke);
UIImage*image =UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
returnimage;
}
给一个图片做出圆角:
@implementationUIImageView (CornerRounder)
- (void)dr_addCornerRadius:(CGFloat)radius {
self.image= [self.image dr_imageAddCornerWithRadius:radius andSize:self.bounds.size];
}
@end
@implementationUIImage (ImageCornerRounder)
- (UIImage*)dr_imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
CGRect rect =CGRectMake(0,0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(size,NO, [UIScreen mainScreen].scale);
CGContextRefctx =UIGraphicsGetCurrentContext();
UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CGContextAddPath(ctx,path.CGPath);
CGContextClip(ctx);
[self drawInRect:rect];
CGContextDrawPath(ctx,kCGPathFillStroke);
UIImage* newImage =UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
returnnewImage;
}
@end
这样就可以就可以有效避免大量的离屏渲染拖慢fps。
其中 CGContextAddArcToPoint用法:
void CGContextAddArcToPoint(CGContextRef c,CGFloatx1,CGFloaty1,CGFloatx2,CGFloaty2,CGFloatradius)
下图中,P1 是当前路径所在的点,坐标是(x,y)
P1(x,y)和(x1,y1)构成切线1,(x1,y1)和(x2,y2)构成切线2, r 是上面函数中的radius, 红色的线就是CGContextAddArcToPoint绘制的曲线. 它不会画到 (x2, y2)这个点, 绘制到圆弧的终点就会停止.
当然,在圆角较少的情况下大可不必这么折腾,设置圆角的方法也有很多种,除了最常用的layer两个属性以外,还有:
1.UIBezierPath:
- (void)drawRect:(CGRect)rect {
CGRect bounds =self.bounds;
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.
0] addClip];
[self.image drawInRect:bounds];
}
2.maskLayer(CAShapeLayer)
- (void)drawRect:(CGRect)rect {
UIBezierPath *maskPatch = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(10, 10)]; //这里的第二个参数可以设置圆角的位置,这里是设置左上和右上两个圆角
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPatch.CGPath;
self.layer.mask = maskLayer;
}
这里要感谢叶孤城大神的文章解疑,本文圆角设置方法介绍不算全面,还有待补充,只是最近项目上遇到了百年一遇的离屏渲染导致了严重卡屏问题所以就小研究了一下,并做下笔记。
----------------------------华丽的时间分割线------------------------------
2月15日补充:阴影效果的离屏渲染
给UIView及其子类添加阴影效果的时候,通常我们是这么做的:
//阴影的颜色
self.imageView.layer.shadowColor= [UIColor blackColor].CGColor;
//阴影的透明度
self.imageView.layer.shadowOpacity=0.8f;
//阴影的圆角
self.imageView.layer.shadowRadius=4;
//阴影偏移量
self.imageView.layer.shadowOffset=CGSizeMake(0,0);
这里就像上文所说的,直接设置了shadowOffset,因此导致了离屏渲染。
解决办法:
用shadowPath代替。
self.imageView.layer.shadowPath = CGPathCreateWithRect(self.imageView.layer.bounds, nil);
或者可以自定义路径阴影
UIBezierPath*path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(-5, -5)];
//添加直线
[path addLineToPoint:CGPointMake(imageWidth /2, -15)];
[path addLineToPoint:CGPointMake(imageWidth +5, -5)];
[path addLineToPoint:CGPointMake(imageWidth +15, imageHeight /2)];
[path addLineToPoint:CGPointMake(imageWidth +5, imageHeight +5)];
[path addLineToPoint:CGPointMake(imageWidth /2, imageHeight +15)];
[path addLineToPoint:CGPointMake(-5, imageHeight +5)];
[path addLineToPoint:CGPointMake(-15, imageHeight /2)];
[path addLineToPoint:CGPointMake(-5, -5)];
//设置阴影路径
self.imageView.layer.shadowPath= path.CGPath;