离屏渲染机制描述及界面优化
GPU渲染机制
CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
ios_screen_display.png ios_vsync_runloop.png
GPU屏幕渲染方式
GPU屏幕渲染方式有两种:
-
On-Screen Rendering
(当前屏幕渲染)
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。 -
Off-Screen Rendering
(离屏渲染)
指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就需要使用到离屏渲染。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
-
创建新的缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。 -
上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
-
创建新的缓冲区
既然离屏渲染这么耗性能,为什么有这套机制呢?
有些效果被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。图层属性的混合体没有预合成之前不能直接在屏幕中绘制,就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。
离屏渲染的触发
以下情况或操作会触发离屏渲染:
- 1、masks(遮罩),为图层设置遮罩
layer.mask
- 2、图层截取,将图层的
layer.masksToBounds
或view.clipsToBounds
属性设置为true
- 3、透明设置,将图层
layer.allowsGroupOpacity
属性设置为YES
和layer.opacity
小于1.0 - 4、shadows(阴影),为图层
设置阴影layer.shadow
- 5、开启光栅化,设置
layer.shouldRasterize
为true
- 6、设置圆角,
layer.cornerRadius
- 7、设置抗锯齿,
layer.edgeAntialiasingMask
,layer.allowsEdgeAntialiasing
- 8、文本,(任何种类,包括UILabel, CATextLayer, Core Text等)
- 9、渐变
- 10、特殊的离屏渲染:CPU渲染
如果重写了drawRect
方法,并且使用任何Core Graphics
的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步的完成,渲染得到的bitmap最后再交由GPU用于显示。CoreGraphic
通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程。
光栅化概念
其中shouldRasterize(光栅化)
是比较特别的一种:
光栅化概念:将图转化为一个个栅格组成的图像。
光栅化特点:每个元素对应帧缓冲区中的一像素。
shouldRasterize = YES
在其他属性出发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayer没有发生改变,在下一帧可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。
相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。
当你使用光栅化时,你可以开启Color Hits Green and Misses Red
来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。
如果光栅化的层变红的太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁。红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。
注意:对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。例如我们日常经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化,则会造成大量的离屏渲染,降低图形性能。
离屏渲染的检测
怎么检测离屏渲染呢?我们可以利用Instruments的Core Animation来检测离屏渲染。通过选择Xcode --> Debug --> View Debugging -->Rendering 选择离屏渲染属性,运行项目即可检测离屏渲染。具体各个属性解释可以看这篇文章:iOS Instrument使用之Core Animation
离屏渲染检测.png我们来看看跟离屏渲染相关的几个属性设置:
-
Color Offscreen-Rendered Yellow
开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。 -
Color Hits Green and Misses Red
如果shouldRasterize
被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。
该选择哪种渲染方式?
1、尽量使用当前屏幕渲染
离屏渲染、CPU渲染可能带来性能问题,一般情况下,我们要尽量使用当前屏幕渲染。
2、离屏渲染和CPU渲染
由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。
离屏渲染优化
设置圆角
方法一
我们通常会采用这种方式来设置圆角:
/**
设置cornerRadius>0且clipToBounds为YES,再添加子视图
*/
- (void)setCorner1{
self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.width/2;
self.avatarImageView.clipsToBounds = YES;
// 或通过设置 layer.masksToBounds = YES
// self.avatarImageView.layer.masksToBounds = YES;
// 如果再添加子视图会触发离屏渲染,不添加则不会
[self.avatarImageView addSubview:self.titleLabel];
}
我们通常设置圆角会通过设置layer.cornerRadius和layer.masksToBounds = YES来设置。这样设置在视图没有子视图的情况下是不会触发离屏渲染的,有子视图就会触发离屏渲染。有子视图的情况下还需要寻找别的方式来避免离屏渲染。
其实在iOS9.0之前UIimageView跟UIButton像上面这样设置圆角都会触发离屏渲染。
iOS9.0系统优化之后UIButton像上面这样设置圆角还是会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
方法二
利用CoreGraphics
画一个圆形上下文,然后把图片绘制上去,得到一个圆形的图片,达到切圆角的目的。
- (UIImage *)drawCircleImage:(UIImage*)image
{
CGFloat side = MIN(image.size.width, image.size.height);
UIGraphicsBeginImageContextWithOptions(CGSizeMake(side, side), false, [UIScreen mainScreen].scale);
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, side, side)].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
CGFloat marginX = -(image.size.width - side) * 0.5;
CGFloat marginY = -(image.size.height - side) * 0.5;
[image drawInRect:CGRectMake(marginX, marginY, image.size.width, image.size.height)];
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
方法三
利用mask
设置圆角,利用的是UIBezierPath
和CAShapeLayer
来完成,不过这种方式也会造成离屏渲染。
CAShapeLayer *mask = [[CAShapeLayer alloc] init];
mask.opacity = 1.0;
mask.path = [UIBezierPath bezierPathWithOvalInRect:self.avatarImageView.bounds].CGPath;
self.avatarImageView.layer.mask = mask;
设置阴影
阴影可以通过设置layer
层的shadowXXX
属性,就可以很方便的UIView
添加阴影效果,但是不同的设置方式可能产生性能方面的问题。下面介绍一下不同方式对性能的影响。
方法一
通过设置下面的4个属性,就可以添加阴影,不过这种方式会造成离屏渲染
。因为绘制阴影而不指定阴影路径,在绘制阴影的过程中就会产生大量的离屏渲染,非常消耗性能,从而造成UI卡顿。
如下方式设置阴影造成离屏渲染
的原因是:iOS会先绘制目标的阴影,然后绘制目标本身,在没有指定阴影的绘制路径时,iOS视图在每次绘制前都会递归的精确计算每个子层阴影的路径,这会非常消耗性能,也是导致卡顿的根源。
// 设置阴影颜色
shadowImgView.layer.shadowColor = [UIColor redColor].CGColor;
// 设置阴影透明度
shadowImgView.layer.shadowOpacity = 0.8f;
// 设置阴影偏移量,默认是(0,-3),向上偏移
shadowImgView.layer.shadowOffset = CGSizeMake(5, 5);
// 设置阴影半径
shadowImgView.layer.shadowRadius = 5.f;
方法二
为了减少因为没有设置shadowPath
造成绘制阴影时大量重复绘制的问题,我们可以指定阴影的绘制路径,这样在绘制阴影时,就可以在多个layer层共享同一个路径的阴影,以此来提高性能。
如果不指定路径shadowPath
,就会使用layer
层的alpha
通道的混合,而如果指定阴影的路径,就会在多个layer
之间共享同一路径,以此来提高性能。
有关什么是layer
层的混合,可以这样理解:iOS在渲染每一帧时,都会计算每一个像素的颜色,如果上层layer不透明,就只取上层layer的颜色;而如果上层layer存在透明度时(alpha通道),则需要混合每一层的颜色来计算最终的颜色。如果layer越多,计算量就越大,也就比较耗性能。所以,在开发中,要尽量减少视图的透明层。具体代码如下:
// 设置阴影颜色
shadowImgView.layer.shadowColor = [UIColor redColor].CGColor;
// 设置阴影透明度
shadowImgView.layer.shadowOpacity = 0.8f;
// 设置阴影偏移量,默认是(0,-3),向上偏移
shadowImgView.layer.shadowOffset = CGSizeMake(5, 5);
// 设置阴影半径
shadowImgView.layer.shadowRadius = 5.f;
// 设置阴影路径
UIBezierPath *path = [UIBezierPath bezierPathWithRect:shadowImgView.bounds];
// 如果是圆形view,则使用下面的圆形路径
//UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:shadowImgView.bounds];
shadowImgView.layer.shadowPath = path.CGPath;
注意:xib拖出来的控件是没法像上面这样设置阴影的,具体设置阴影的方法看这篇文章iOS xib设置阴影
参考文章:
1.ios中的离屏渲染与相关性能检测优化
2.iOS离屏渲染之优化分析
3.iOS性能优化-离屏渲染
4.iOS"离屏渲染"整理总结
5.iOS的阴影绘制及性能优化