ios--离屏渲染详解
目录:
- 1.图像显示原理
- 2.图像显示原理
- 2.1 图像到屏幕的流程
- 2.2 显示器显示的流程
- 3.卡顿、掉帧
- 3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering
- 2.3 掉帧和屏幕卡顿的本质
- 4.离屏渲染
- 4.1 什么事离屏渲染、离屏渲染的过程
- 4.2 既然离屏渲染影响界面,为什么还要用
- 5.触发离屏渲染
- 6.如何优化
1.引言
先来聊聊为什么要了解离屏渲染?
看看现在app开发的大环境,14年的时候在深圳,基本上每个公司都要做一个app。不做一个app你都不一定能拉倒更多的投资。再看看现在,死了一大半,现在的用户也不想去下载太多的app。一般手机上只留一些常用的,基本全是大厂的app。然后ios这行问的也就越来越难。性能优化这个绝对会问,在网上也有许多性能优化的总结,但是你不能不知道为什么这么做能优化,要知道其为什么。那么,这时候你就需要知道界面是怎么渲染的,什么时候会掉帧,什么时候会卡顿,这些都使得我们非常有必要去了解离屏渲染。
离屏渲染过程
2.图像显示原理
2.1 图像到屏幕的流程
先来看一张图,我们结合这张图来说
Core Animation 流水线.png
首先要明白的一个东西是Render Server 进程,app本身其实并不负责渲染,渲染是有独立的进程负责的,它就是Render Server 。
当我们在代码里设置修改了UI界面的时候,其实它本质是通过Core Animation修改CALayer。在后续的核心动画总结中 我们会说到UIView和CALayer的关系,以及核心动画的设置等等,这个知识点有点多,需要单独详细的总结出来。所以最后按照图片中的流程显示。
- 首先,有app处理事件(Handle Events),例如:用户点击了一个按钮,它会触发其他的视图的一个动画等
- 其次,app通过CPU完成对显示内容的计算 例如:视图的创建,视图的布局 图片文本的绘制等。在完成了对显示内容的计算之后,app对图层进行打包,并在下一次Runloop时,将其发送至Render Server
- 上面我们提到,Render Server负责渲染。Render Server通过执行Open GL、Core Graphics Metal相关程序。 调用GPU
- GPU在物理层完成了对图像的渲染。
说到这我们就要停一下,我们来看下一个图
GPU.png
上面的流程图 细化了GPU到控制器的这一个过程。
GPU 拿到位图后执行顶点着色、图元装配、光栅化、片段着色等,最后将渲染的结果交到了Frame Buffer(帧缓存区当中)
然后视频控制器从帧缓存区中拿到要显示的对象,显示到屏幕上
图片中的黄色虚线暂时不用管,下面在说垂直同步信号的时候,就明白了。
这是从我们代码中设置UI,然后到屏幕的一个过程。
2.2 显示器显示的过程
现在从帧缓存中拿到了渲染的视图,又该怎么显示到显示器上面呢?
从图中我们也能大致的明白显示的一个过程。
显示器的电子束从屏幕的左上方开始逐行显示,当第一行扫描完之后接着第二行 又是从左到右,就这样一直到屏幕的最下面扫描完成。我们都知道。手机它是有屏幕的刷新次数的。安卓的现在好多是120的,ios是60。1秒刷新60次,当我们扫描完成以后,屏幕刷新,然后视图就会显示出来。
3.UI卡顿 掉帧
3.1垂直同步 Vsync + 双缓冲机制 Double Buffering
首先我们了解了上面渲染的过程以后,需要考虑遇到一些特别的情况下,该怎么办?在我们代码里写了一个很复杂的UI视图,然后CPU计算布局、GPU渲染,最后放到缓存区。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 。
那么已扫描的部分就是上一帧的画面,而未扫描的部分就是新一帧的图像,这样是不是就造成了屏幕撕裂了。
但是,在我们平常开发的过程遇到过屏幕撕裂的问题吗?没有吧,这是为什么呢?
显然是苹果做了优化操作了。也就是垂直同步 Vsync + 双缓冲机制 Double Buffering。
垂直同步 Vsync
垂直同步 Vsync相当于给帧缓存加了锁,还记得上面说到的那个黄色虚线嘛,在我们扫描完一帧以后,就会发出一个垂直同步的信号,通知开始扫描下一帧的图像了。他就像一个位置秩序的,你得给我排队一个一个来,别插队。插队的后果就是屏幕撕裂。
双缓冲机制 Double Buffering
扫描显示排队进行了,这样在进行下一帧的位图传入的时候,也就意味着我要立刻拿到位图。不能等CPU+GPU计算渲染后再给位图,这样就影响性能。要怎么解决这个问题呢?肯定是 在你快要渲染之前你就要把这些都完成了。你就像排队打针一样,为了节省时间肯定事先都会挽起袖子,到医生那时,直接一针下去了事。扯远了 哈哈。想预先渲染好,就需要另外一个缓存来放下一帧的位图,在它需要扫描的时候,再把渲染好的位图给了帧缓存,帧缓存拿到以后 开始快乐的扫描 显示。
一个图解释
3.2 掉帧卡顿
垂直同步和双缓存机制完美的解决了屏幕撕裂的问题,但是又引出一个新的问题:掉帧。
掉帧是什么意思呢?从网上copy了一份图
掉帧.png
其实很好理解,上面我们说了ios的屏幕刷新是60次,那么在一次刷新的过程中,我们CPU+GPU它没有把新渲染的位图放到帧缓存区,这时候是不是还是显示的原来的图像。当下刷新下一帧的时候,拿到了新的位图,这里是不是就丢失了一帧。
卡顿的根本原因:
CPU和GPU渲染流水线耗时过长 掉帧
我们平常写界面的时候,通过一些开源的库或者自己使用runloop写的库来检测界面卡顿的时候,屏幕刷新率在50以上就很可以了。一般人哪能体验到掉了10帧。你要刷新率是30,那卡顿想过就很明显了。
4 离屏渲染
4.1什么是离屏渲染 离屏渲染的过程
是指在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.
过程:首先会创建一个当前屏幕缓冲区以外的新缓存区,屏幕渲染会有一个上下文环境,离屏渲染的过程就是切花上下文环境,现充当前屏幕切换到离屏,等结束以后又将上下文切换回来。所以需要更长的时间来处理。时间一长就可能造成掉帧。
并且 Offscreen Buffer离屏缓存 本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。而且离屏缓存区并不是没有限制大小的,它是不能超过屏幕总像素的2.5倍。
4.2为什么要使用离屏渲染
1.一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
2.处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个VSync信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染。
5.触发离屏渲染
- 为图层设置遮罩(layer.mask)
- 图层的layer. masksToBounds/view.clipsToBounds属性设置为true
- 将图层layer. allowsGroupOpacity设置为yes和layer. opacity<1.0
- 为图层设置阴影(layer.shadow)
- 为图层设置shouldRasterize光栅化
6 复杂形状设置圆角等
7 渐变
8 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
9 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
6 离屏渲染的优化
1 圆角优化
方法一
iv.layer.cornerRadius = 30;
iv.layer.masksToBounds = YES;
方法二
利用mask设置圆角,利用贝塞斯曲线和CAShapeLayer来完成
CAShapeLayer *mask1 = [[CAShapeLayer alloc] init];
mask1.opacity = 0.5;
mask1.path = [UIBezierPath bezierPathWithOvalInRect:iv.bounds].CGPath;
iv.layer.mask = mask1;
方法三
利用CoreGraphics画一个圆形上下文,然后把图片绘制上去
- (void)setCircleImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage * circleImage = [image imageWithCircle];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = circleImage;
});
});
}
#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一张圆形图片
- (instancetype)imageWithCircle
{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
[path addClip];
[self drawAtPoint:CGPointZero];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
}
shadows(阴影)
设置阴影后,设置CALayer的shadowPath
view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;
mask(遮罩)
不使用mask
使用混合图层 使用混合图层,在layer上方叠加相应mask形状的半透明layer
sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
allowsGroupOpacity(组不透明)
关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度
edge antialiasing(抗锯齿)
不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)
当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;
如果视图内容是动态变化的,例如cell中的图片,这个时候使用光栅化会增加系统负荷。