OpenGL离屏渲染
先看一个例子
1.背景图片
UIButton * btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 100, 100, 100);
btn1.layer.cornerRadius = 50;
[btn1 setImage:[UIImage imageNamed:@"截屏2020-07-07 上午11.05.22"] forState:0];
[self.view addSubview:btn1];
btn1.clipsToBounds = YES;
2.背景颜色加背景图片
btn2.frame = CGRectMake(100, 230, 100, 100);
btn2.layer.cornerRadius = 50;
btn2.backgroundColor = [UIColor systemBlueColor];
[btn2 setImage:[UIImage imageNamed:@"截屏2020-07-07 上午11.05.05"] forState:0];
[self.view addSubview:btn2];
btn2.clipsToBounds = YES;
3.不存在背景图片
btn3.frame = CGRectMake(100, 350, 100, 100);
btn3.layer.cornerRadius = 50;
btn3.backgroundColor = [UIColor systemBlueColor];
[self.view addSubview:btn3];
btn3.clipsToBounds = YES;
4.只设置图片无背景色
btn4.frame = CGRectMake(100, 480, 100, 100);
btn4.layer.cornerRadius = 50;
[btn4 setImage:[UIImage imageNamed:@"截屏2020-07-07 上午11.05.05"] forState:0];
[self.view addSubview:btn4];
btn4.clipsToBounds = YES;
打开模拟器离屏渲染
其中1,3,4产生了离屏渲染
模拟器离屏渲染
离屏渲染的原理
1.app渲染流程
渲染流水线示意图
看上图得知Application以及RenderServer部分是运行在CPU上的,Application处理好数据后,提交到Render Server,然后Core Animation在会将具体操作转换成GPU的Draw calls(OpenGL/Metal);也就是CPU+GPU共同完成渲染工作。
这个时候我们再来看什么叫离屏渲染
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染(offscreen buffer)
一般的渲染流程简化图
渲染流程简化图
离屏渲染流程简化图
离屏渲染流程简化图
离屏渲染性能问题:
1),需要额外的存储空间,
2),将结果从Offscreen buffer转存到Frame Buffer中也是需要时间的
既然离屏渲染容易带来性能问题,为什么还要用呢?
1.非常多的特殊效果,并不能一次用一个图层就能画出来,所以需要使用额外的offscreen Buffer来保持中间状态(不得不使用),比如:圆角,阴影
2.能带来效率的优势:既然效果会多次出现在屏幕上,可提前渲染好,保存在offscreen Buffer中,从而达到复用的结果 比如:光珊化shouldRasterize
引发离屏渲染的情况
1.使用了 mask 的 layer (layer.mask)
2.需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
设置了layer.cornerRadius,只会设置backgroundColor和border的圆角,不会设置content的圆角,除非同时设置了layer.masksToBounds
3.设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
4.添加了投影的 layer (layer.shadow*)
5.采用了光栅化的 layer (layer.shouldRasterize)
1).如果layer不被复用,则没有必要打开光栅化
2).如果layer不是静态的,需要被频繁修改,比如处于动画之中,那么开启离屏渲染会影响动画效果了。
3).离屏渲染缓存有时间限制,缓存内容100ms内如果没有被使用,那么它就会丢弃,就无法再复用了。
4).离屏渲染缓存空间有限,超过2.5倍屏幕像素大小也会失效。且无法进行复用了。
6.绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
最后来点干货
iOS上的圆⻆处理⼿段参考⽅案
圆⻆触发的离屏渲染 offscreen Rendering
圆⻆触发的离屏渲染
GPU离屏渲染,“画家画法”
通过渲染流水线示意图中我们可以看到,主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销)
然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,**对于每一层****layer****,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改,剪裁操作。
方案1
_imageView.clipsToBounds=YES;
_imageView.layer.cornerRadius=4.0;
方案2
圆角处理2
方案3
圆角处理3[图片上传中...(截屏2020-07-07 下午2.16.40.png-ea8b3a-1594102603503-0)]
方案4
圆角处理4
- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius
corners:(UIRectCorner)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
borderLineJoin:(CGLineJoin)borderLineJoin {
if (corners != UIRectCornerAllCorners) {
UIRectCorner tmp = 0;
if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
corners = tmp;
}
UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);
CGFloat minSize = MIN(self.size.width, self.size.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, borderWidth)];
[path closePath];
CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, self.CGImage);
CGContextRestoreGState(context);
}
if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius,
borderWidth)];
[path closePath];
path.lineWidth = borderWidth;
path.lineJoinStyle = borderLineJoin;
[borderColor setStroke];