一、画面撕裂
1.1画面撕裂的形成
在介绍离屏渲染
之前我们先了解一下什么是画面撕裂
,以及其形成的原因:
在游戏中我们有时会遇到这样的画面,我们很明显的能看到画面存在撕裂问题,其形成的原因是: GPU
渲染之后会将结果放在 帧缓存区
中,视频控制器
再通过读取帧缓存区
中的数据进行 数模转换
来显示在屏幕上,显示的过程是从上至下逐行扫描进行显示,如下图画面撕裂的形成过程:假设只有一个帧缓存区的情况下,帧缓存区首先放了图1,屏幕首先对图1进行由上至下的扫描,但在扫描到图2的位置时,GPU
又渲染了一张新的图片放到了帧缓存区中(图3),此时屏幕将会继续图2的位置进行扫描帧缓存区中的图片,即是此时的图3,则屏幕通过由上至下的扫描最终得到的结果就将是图4展示的样子,此时即形成了画面撕裂
。
1.2苹果解决画面撕裂的策略
苹果为应对画面撕裂问题采取了垂直同步
+双缓存
的策略。
垂直同步(Vertical synchronization)
:在扫面的过程中加入垂直同步信号
,确保只有当前帧的图片扫面完成之后才会继续扫面下一帧的图片。
双缓存区
:即采用两个缓存区来存储图片,屏幕交替扫描两个缓存区来进行显示。
虽然垂直同步
+双缓存
的策略解决了画面撕裂
问题,但同时也引入了另一个问题:掉帧
。掉帧
最直观的体现就是屏幕的卡顿,其形成的原因是:当接收到垂直同步信号
的时候,CPU
和GPU
还没有准备好相应的数据,即此时帧缓存区(FrameBuffer)
不存在将要显示的数据,视频控制器拿不到新的数据,就会重复对上一帧的数据进行渲染
。
为了应对掉帧
问题,人们又采用的三缓存区
,但掉帧
归根结底的主要原因是CPU
和GPU
处理速度问题,三缓存区
虽然能在一定程度上抑制掉帧
问题,但并不能从根本上解决。
1.3屏幕卡顿的原因
-
CPU
和GPU
渲染流水线耗时过长,造成掉帧
; -
垂直同步
+双缓存
的策略以掉帧
为代价来解决屏幕撕裂问题; -
三缓存区
更合理的使用CPU
和GPU
,减少掉帧
的次数,但是并不能从根本上解决掉帧
问题。
二、离屏渲染
2.1离屏渲染的触发
我们一般认为圆角
会触发离屏渲染
,但设置圆角
就一定会触发离屏渲染
吗?首先我们来看一个简单的demo:
- (void)viewDidLoad {
[super viewDidLoad];
//1.按钮存在背景图片
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(100, 130, 100, 100);
btn1.layer.cornerRadius = 50;
[self.view addSubview:btn1];
[btn1 setImage:[UIImage imageNamed:@"image"] forState:UIControlStateNormal];
btn1.clipsToBounds = YES;
//2.按钮不存在背景图片
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(100, 280, 100, 100);
btn2.layer.cornerRadius = 50;
btn2.backgroundColor = [UIColor redColor];
[self.view addSubview:btn2];
btn2.clipsToBounds = YES;
//3.UIImageView 设置了图片+背景
UIImageView *imageV1 = [[UIImageView alloc] init];
imageV1.frame = CGRectMake(100, 430, 100, 100);
imageV1.backgroundColor = [UIColor blueColor];
[self.view addSubview:imageV1];
imageV1.layer.cornerRadius = 50;
imageV1.layer.masksToBounds = YES;
imageV1.image = [UIImage imageNamed:@"image"];
//4.UIImageView 只设置了图片 无背景色
UIImageView *imageV2 = [[UIImageView alloc] init];
imageV2.frame = CGRectMake(100, 580, 100, 100);
[self.view addSubview:imageV2];
imageV2.layer.cornerRadius = 50;
imageV2.layer.masksToBounds = YES;
imageV2.image = [UIImage imageNamed:@"image"];
}
运行,并设置模拟器,Debug
-> Color Off-screen rendered
标记出离屏渲染
的部分:
被标记出黄色的部分是触发了离屏渲染
的,而未被标记的则没有触发离屏渲染
,由此可见设置了圆角
不一定就会触发离屏渲染
,那么触发离屏渲染
的条件到底是什么呢?
2.2离屏渲染的探究
通常情况下的渲染流程是这样的: APP渲染流程APP
通过CPU
和GPU
的合作,不断的将渲染的内容放到帧缓冲区(Frame Buffer)
中,屏幕不断的从帧缓冲区
中拿到要展示的内容,实时的显示在屏幕上。
离屏渲染
的流程是这样的:
与普通的渲染不同,离屏渲染
需要创建额外的离屏渲染缓冲区(offscreen Buffer)
,将渲染好的内容放入其中,再等到合适的时机将离屏渲染缓冲区
中的内容进行叠加、合并,之后再放入帧缓冲区
中。
从流程图我们可以看出,离屏渲染
时,需要APP
提前将部分渲染能容保存到离屏渲染缓冲区
,必要的时候需要对Offscreen Buffer
和Frame Buffer
进行切换,所以势必需要更多的处理时间,而且由于离屏渲染
需要开辟额外的空间,大量的离屏渲染
对势必也会消耗大量的内存。与此同时,离屏渲染缓冲区
也是有大小限制的,不能超过屏幕像素点的2.5倍。
大量的离屏渲染
容易造成掉帧
,所以很多情况下我们能避则避。但有时我们需要实现一些特殊的效果,需要Offscreen Buffer
保存渲染的中间状态时,我们也不得不使用离屏渲染
。
以苹果提供的毛玻璃效果UIBlurEffectView
为例:
整个过程需要经历,渲染内容->捕获内容->水平模糊->垂直模糊->合并形成毛玻璃效果,根据我们对帧缓冲区
的了解,为节省空间,帧缓冲区中
的内容绘制到屏幕上之后就会直接移除,无法做到如此复杂的特效,该过程需要在离屏缓冲区
进行处理。
有时我们也会为了提高复用效率通过layer的光栅化 shouldRasterize
主动开启离屏渲染
,苹果关于shouldRasterize
的解释如下:
When the value of this property is YES , the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.
开启光栅化
后,会触发离屏渲染
,Render Serve
r 会强制将 CALayer
的渲染位图结果bitmap
保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap
包含layer
的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果layer
的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化
。
圆角、阴影、组透明度等会由系统自动触发离屏渲染
,那么打开光栅化
可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染
,所以相比之下会多花费第一次离屏渲染
的时间,但是可以节约后续的重复渲染的开销。
但shouldRasterize
的使用也有一定的限制:
- 如果
layer
不能被复用,则没有必要打开光栅化
; - 如果
layer
不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染
反而影响效率; -
离屏渲染
缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用; -
离屏渲染
缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用。
layer的构成
由上图我们可以看出layer
由三部分组成,通常我们设置圆角会设置layer
的cornerRadius
,关于cornerRadius
,apple
的解释如下:
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
由上述可知,如果我们只是设置了cornerRadius
属性,并不会对content
进行裁剪,只有我们设置masksToBounds
才会对内容进行裁剪。
图层的叠加大致遵循“画家算法”
,即由远及近绘制图层显示在屏幕上:
我们可以试想下:如果我们要对一个拥有多个图层构成的视图进行圆角设置,如果是存在帧缓冲区
,那么就会存在一个问题,每渲染一帧就会丢前面的一帧数据,当我们要设置圆角时,前面的图层早已丢失,而离屏缓存区
不同,离屏缓存区
会对渲染的图层保留一段时间,这段时间就足以我们对多图层进行、合并、设置圆角等操作。想要触发离屏渲染
不单单是说设置了masksToBounds
就会触发,我们更多的要在意的是我们所要操作的图层,是否需要保留中间图层,如果只是单图层,肯定不会触发离屏渲染
。
值得注意的是,重写 drawRect
: 方法并不会触发离屏渲染。重写 drawRect
:会将 GPU
中的渲染操作转移到 CPU
中完成,并且需要额外开辟内存空间。
2.3圆角处理的参考方案
- 方案一:
最简单的方法就是找UI切带圆角的图片。 - 方案二:
- (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius {
CGFloat w = self.size.width;
CGFloat h = self.size.height;
CGFloat scale = [UIScreen mainScreen].scale;
//防止圆角半径小于0,或者大于宽/高中较小值的一半。
if (cornerRadius < 0) {
cornerRadius = 0;
}else if (cornerRadius > MIN(w, h)/2.0){
cornerRadius = MIN(w, h)/2.0;
}
UIImage *image = nil;
CGRect imageFrame = CGRectMake(0, 0, w, h);
UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
[[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
[self drawInRect:imageFrame];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- 方案三
+ (UIImage *)addMaskToBounds:(CGRect)maskBounds image:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
CGFloat w = maskBounds.size.width;
CGFloat h = maskBounds.size.height;
CGSize size = maskBounds.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGRect imageRect = CGRectMake(0, 0, w, h);
if (cornerRadius < 0) {
cornerRadius = 0;
}else if (cornerRadius > MIN(w, h)/2.0){
cornerRadius = MIN(w, h)/2.0;
}
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
[[UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:cornerRadius] addClip];
[image drawInRect:imageRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- 方案四
@interface RoundImageView()
@property (nonatomic, strong) UIImageView *maskImageView;
@end
@implementation RoundImageView
- (instancetype)init {
self = [super init];
if (self) {
_maskImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_maskImageView.image = [UIImage imageNamed:@"ic_imageView_mask"];//加圆角图片盖在上面
[self addSubview:_maskImageView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect bounds = self.bounds;
_maskImageView.frame = bounds;
}
另附:YYImage的圆角处理
YYImage的圆角处理
2.4常见触发离屏渲染的几种情况
- 使用了
mask
的layer
(layer.mask
) - 需要进行裁剪的
layer
(layer.masksToBounds / view.clipsToBounds
) - 设置了组透明度为Yes,并且透明度不为1的
layer
(layer.allowsGroupOpacity / layer.opacity
) - 添加了投影的
layer
(layer.shadow
) - 采用了
光栅化
的layer
(layer.shouldRasterize
) - 绘制了文字的
layer
(UILabel
,CATextLayer
,CoreText
等)
网友评论