iOS性能优化-离屏渲染
Posted 乌戈勒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS性能优化-离屏渲染相关的知识,希望对你有一定的参考价值。
前言
在使用UIKit的过程中,性能优化是永恒的话题。很多分析优化滑动性能的文章,只介绍了优化方法,却对背后的原理避而不谈,本文对其中原理进行了简单的总结!可以参考我之前写的一篇总结iOS性能优化-理论基础
而离屏渲染是导致性能优化最主要的原因,这篇文章主要总结一下离屏渲染的知识。
不妨思考一下下面的问题,自己是否有一个清晰的认识:
1、界面为什么会卡顿?–从屏幕显示图像的原理去分析(丢帧现象)
2、你怎么检测界面存在卡顿?– 参考KMCGeigerCounter(FPS、CADisplayLink、SKView)
3、为什么频繁调整视图层次、添加和移除视图,会很耗CPU性能?–从UIView(frame/bounds/transform)分析CALayer调用这些属性的内部实现解释
4、为什么在后台线程提前计算好视图布局、并且对视图布局进行缓存,这样能解决性能问题?–同上
5、文本展示为什么用CoreText绘制,CoreText相比UILabel好在哪里?–CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。
6、为什么在setImage之前要先将image进行解码操作?
7、为什么要把控件尽量设置成不透明opaque=YES?–从GPU处理多个视图view/CALayer混合叠加的原理分析
8、为什么设置CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,重写drawRect方法,会影响滑动的流畅性?–从离屏渲染分析
9、什么是离屏渲染?
10、光栅化shouldRasterize和离屏渲染的关系是什么,何时应该使用?
11、怎么对UITableView进行性能优化?–参考FDTemplateLayoutCell预排版、预估高度
12、如何解决CALayer 的 border、圆角、阴影、遮罩这些引起的离屏渲染导致的性能问题?–参考CoreGraphics重绘
13、如何设计一套ios的LRU图片缓存淘汰框架?–参考YYMemoryCache(双向链表)
14、如何解决多线程爆炸的问题?–参考YYDispatchQueuePool(为不同优先级创建和 CPU 数量相同的 serial queue,每次从 pool 中获取 queue 时,会轮询返回其中一个 queue。)
首先,我们要理解一下什么是离屏渲染?
一、离屏渲染的概念
屏幕渲染有以下两种方式:
1、On-Screen Rendering
当前屏幕渲染,指的是在当前用于显示的屏幕缓冲区中进行渲染操作。
2、Off-Screen Rendering
离屏渲染,指的是 GPU 或 CPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
知道什么是离屏渲染之后,那离屏渲染为啥会带来性能损耗呢?
二、离屏渲染为什么会导致性能损耗
离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。
因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。
触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。
过程中需要切换 contexts (上下文环境),先从当前屏幕切换到离屏的contexts,渲染结束后,又要将 contexts 切换回来,而切换过程十分耗费性能。
三、怎么理解上下文切换?
不管是在GPU渲染过程中,还是一直所熟悉的进程切换,上下文切换在哪里都是一个相当耗时的操作。
首先我要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering或者再开始一个新的离屏渲染重复之前的操作。
举例:设置mask怎么牵扯上下文切换?
一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次mask就是普通渲染的30倍以上耗时操作。问我这个30倍以上这个数据怎么的出来的?当我在cell的UIImageView的实例增加到150个,并去掉圆角的时候,帧数才跌至28帧每秒。虽然不是甚准确,但至少反映mask这个耗时是无mask操作的耗时的数十倍的。既然离屏渲染会带来性能损耗,那什么操作会导致离屏渲染呢?
四、什么操作会导致离屏渲染
1、GPU 产生的离屏渲染主要是当 CALayer 使用圆角,阴影,遮罩等属性的的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中渲染,则过程中需要进行离屏渲染。
2、CPU 产生的离屏渲染主要由Core Graphics API(核心绘图)的使用导致。
既然知道哪些操作会导致离屏渲染,首先要知道一些知识
五、UIView和CALayer的关系
1、简单来说,UIView 是对 CALayer 的一个封装。
CALayer 负责显示内容contents,UIView 为其提供内容,以及负责处理触摸等事件,参与响应链。
CALayer 有三个视觉元素,中间的contents属性是这样声明的:var contents: AnyObject?,实际上它必须是一个CGImage才能显示。
CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成。
当使用let view = UIView(frame: CGRectMake(0, 0, 200, 200))生成一个视图对象并添加到屏幕上时,从 CALayer 的结构可以知道,这个视图的 layer 的三个视觉元素是这样的:contents为空,背景颜色为空(透明色),前景框宽度为0的前景框,这个视图从视觉上看什么都看不到。
2、撸一撸在iOS程序上图形显示的原理
在iOS系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。
但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。
在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。
实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级API。
可以说CALayer是UIView的内部实现细节。
CALayer其实也只是iOS当中一个普通的类,它也并不能直接渲染到屏幕上,因为屏幕上你所看到的东西,其实都是一张张图片。
而为什么我们能看到CALayer的内容呢,是因为CALayer内部有一个contents属性。
contents默认可以传一个id类型的对象,但是只有你传CGImage的时候,它才能够正常显示在屏幕上。所以最终我们的图形渲染落点落在contents身上。
contents也被称为寄宿图,除了给它赋值CGImage之外,我们也可以直接对它进行绘制。
3、UIView和CALayer的区别
CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。
它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在,它的API虽然提供了“某点是否在图层范围内的方法”,但是它并不具有响应的能力。
4、UIImageView和CALayer又是什么关系?
既然直接对 CALayer 的contents属性赋值一个CGImage便能显示图片,所以 UIImageView 就顺利成章地诞生了。实际上 UIImage 就是对 CGImage(或者 CIImage) 的一个轻量封装。
接下来分析导致离屏渲染的各种操作
六、圆角CornerRadius
1、设置圆角的方法如下:
label.layer.cornerRadius = 5
label.layer.masksToBounds = true
2、如果不设置masksToBounds=YES,是不会有圆角效果的,为什么呢,可以看下cornerRadius的官方解释:
在默认情况下,cornerRadius这个属性只会影响视图的背景颜色和 border。
cornerRadius只对前景框和背景色起作用,再看 CALayer 的结构,如果contents有内容或者内容的背景不是透明的话,还需要把这部分弄个角出来,不然合成的结果还是没有圆角,所以才要修改masksToBounds为true(在 UIView 上对应的属性是clipsToBounds)。
系统圆角需要裁剪 layer 中间的contents。
3、解决方案:
1、如果不需要对外部来源的图片做圆角,由设计师直接画成圆角图片是最方便的;
2、混合图层:在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。
3、通过在后台线程用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;
4、如何在文本视图类上实现圆角?
文本视图主要是这三类:UILabel, UITextField, UITextView。
想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的contents呈现透明的背景色,文本视图类的 layer 的contents默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。
//不要这么做:label.backgroundColor = aColor 以及不要在 IB 里为 label 设置背景色
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
六、阴影ShadowOffset
1、阴影是如何与视图本身结合的?
阴影直接合成在视图的下面,视图结构里并没有多出一个视图。在没有指定阴影路径时,阴影是沿着视图的非透明部分扩展的,而且 CALayer 的三个视觉元素至少有一个存在时才会有阴影。
let imageViewLayer = avatorView.layer
imageViewLayer.shadowColor = UIColor.blackColor().CGColor
imageViewLayer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示
imageViewLayer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响
imageViewLayer.shadowOffset = CGSize(width: 5, height: 5)
2、解决方案:
1、设定阴影路径:与视图的边界相同
2、用圆角优化里混合图层的方法模拟阴影的效果:放一个同样效果的视图在要添加阴影程度的视图的下方
//shadowPath替代shadowOffset
imageViewLayer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath//路径默认为 nil
七、遮罩Mask
也会导致离屏渲染
解决方案:
使用混合图层替代
八、光栅化shouldRasterize
这是一个CALayer离屏渲染终极解决方案。
1、当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便。
view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;
2、但当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。
九、抗锯齿allowsEdgeAntialiasing
少用,不解释
十、allowsGroupOpacity
少用,不解释
以上是关于iOS性能优化-离屏渲染的主要内容,如果未能解决你的问题,请参考以下文章
iOS沉思录UIImage圆角矩形的‘离屏渲染’和‘在屏渲染’实现方法