文档归纳: http://download.csdn.net/detail/bravegogo/9888733
0 iOS 图片显示原理
iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步骤:
1)从磁盘拷贝数据到内核缓冲区
2)从内核缓冲区复制数据到用户空间
3)生成UIImageView,把图像数据赋值给UIImageView
4)如果图像数据为未解码的PNG/JPG,转码为位图数据
5)CATransaction捕获到UIImageView layer树的变化
6)主线程Runloop提交CATransaction,开始进行图像渲染:
A、如果数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐。
B、GPU处理位图数据,进行渲染。
1什么是离屏渲染
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。
屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。
有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。
对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用CAShapeLayer,contentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。
直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。」触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。
离屏渲染合成计算是非常昂贵的, 但有时你也许希望强制这种操作。一种好的方法就是缓存合成的纹理/图层。如果你的渲染树非常复杂(所有的纹理,以及如何组合在一起),你可以强制离屏渲染缓存那些图层,然后可以用缓存作为合成的结果放到屏幕上。
OpenGL中,GPU屏幕渲染有以下两种方式:
· On-ScreenRendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
· Off-ScreenRendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染的代价主要包括两方面内容:
· 创建新的缓冲区
· 上下文的切换,离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
为什么需要离屏渲染?
目的在于当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,即当主屏的还没有绘制好的时候,所以就需要屏幕外渲染,最后当主屏已经绘制完成的时候,再将离屏的内容转移至主屏上。
2哪些操作会触发离屏渲染?
官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。
shouldRasterize(光栅化);masks(遮罩);shadows(阴影);edge antialiasing(抗锯齿);group opacity(不透明);复杂形状设置圆角等;渐变;Text(UILabel, CATextLayer, Core Text, etc)...。
使用 Core Graphics 里的绘制 API 也会触发离屏渲染,比如重写 drawRect:。为什么几年前会产生这样的认识不得而知。在 WWDC 2011:Understanding UIKit Rendering 这个session 里演示了「Core Animation Instruments」里使用「Color Offscreen-Renderd Yellow」选项来检测离屏渲染,在 WWDC 2014:Advanced Graphics and Animations for iOS Apps 也专门演示了这个工具。
Core Graphics 的绘制API的确会触发离屏渲染,但不是那种 GPU的离屏渲染。使用 CoreGraphics绘制 API是在 CPU上执行,触发的是 CPU版本的离屏渲染。
3 UIImageView
既然直接对 CALayer的contents属性赋值一个CGImage便能显示图片,所以 UIImageView 就顺利成章地诞生了。实际上UIImage就是对 CGImage(或者 CIImage)的一个轻量封装。
4 圆角处理
1) 系统圆角
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
2) 如果不需要对外部来源的图片做圆角,由设计师直接画成圆角图片是最方便的;
3) 混合图层:在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。
4) CoreGraphics的 API
-(void)zy_cornerRadiusWithImage:(UIImage *)image
cornerRadius:(CGFloat)cornerRadius
rectCornerType:(UIRectCorner)rectCornerType
backgroundColor:(UIColor *)backgroundColor
{
CGSize size = self.bounds.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius);
UIGraphicsBeginImageContextWithOptions(size, YES, scale);
if
(nil == UIGraphicsGetCurrentContext())
{
return;
}
UIBezierPath *cornerPath =
[UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCornerType cornerRadii:cornerRadii];
UIBezierPath *backgroundRect = [UIBezierPath bezierPathWithRect:self.bounds];
[backgroundColor setFill];
[backgroundRect fill];
[cornerPath addClip];
[image drawInRect:self.bounds];
self.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
5 CPU 限制 VS GPU 限制
当你在屏幕上显示东西的时候,有许多组件参与了其中的工作。其中,CPU和 GPU 在硬件中扮演了重要的角色。在他们命名中 P 和 U 分别代表了”处理”和”单元”,当需要在屏幕上进行绘制时,他们都需要做处理,同时他们都有资源限制(即 CPU 和 GPU 的硬件资源)。
为了每秒达到 60 帧,你需要确定 CPU 和GPU 不能过载。此外,即使你当前能达到 60fps(frame per second),你还是要把尽可能多的绘制工作交给 GPU 做,而让 CPU 尽可能的来执行应用程序。通常,GPU 的渲染性能要比 CPU 高效很多,同时对系统的负载和消耗也更低一些。
既然绘图性能是基于 CPU 和 GPU 的,那么你需要找出是哪一个限制你绘图性能的。如果你用尽了 GPU 所有的资源,也就是说,是 GPU 限制了你的性能,同样的,如果你用尽了 CPU,那就是 CPU 限制了你的性能。
要告诉你,如果是 GPU 限制了你的性能,你可以使用 OpenGL ES Driverinstrument。点击上面那个小的 i 按钮,配置一下,同时注意勾选 Device Utilization %。现在,当你运行你的 app 时,你可以看到你 GPU 的负荷。如果这个值靠近 100%,那么你就需要把你工作的重心放在GPU方面了。
CPU渲染:以上所说的都是离屏渲染发生在OpenGLSE也就是GPU中,但是CPU也会发生特殊的渲染,我们的CPU渲染,也就是我们使用CoreGraphics的时候,但是要注意的一点的是只有在我们重写了drawRect方法,并且使用任何CoreGraphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。
理论上CPU渲染应该不算是标准意义上的离屏渲染,但是由于CPU自身做渲染的性能也不好,所以这种方式也是需要尽量避免的。
6异步绘图
CALayer 有一个叫做drawsAsynchronously的属性,这似乎是一个解决所有问题的高招。注意,尽管这可能提升性能,但也可能让事情变慢。
当你设置 drawsAsynchronously 为 YES 时,发生了什么?你的 -drawRect:/-drawInContext: 方法仍然会被在主线程上调用。但是所有调用 Core Graphics 的操作都不会被执行。取而代之的是,绘制命令被推迟,并且在后台线程中异步执行。
这种方式就是先记录绘图命令,然后在后台线程中重现。为了这个过程的顺利进行,更多的工作需要被做,更多的内存需要被申请。但是主队列中的一些工作便被移出来了(大概意思就是让我们把一些能在后台实现的工作放到后台实现,让主线程更顺畅)。
对于昂贵的绘图方法,这是最有可能提升性能的,但对于那些绘图方法来说,也不会节省太多资源。
7 CALayer
到现在为止,你需要知道在 GPU 内,一个 CALayer 在某种方式上和一个纹理类似。图层有一个后备存储,这便是被用来绘制到屏幕上的位图。
通常,当你使用 CALayer 时,你会设置它的内容为一个图片。这到底做了什么?这样做会告诉 CoreAnimation 使用图片的位图数据作为纹理。如果这个图片(JPEG或PNG)被压缩了,Core Animation 将会这个图片解压缩,然后上传像素数据到 GPU。
尽管还有很多其他种类的图层,如果你是用一个简单的没有设置上下文的 CALayer,并为这个 CALayer 设置一个背景颜色,Core Animation 并不会上传任何数据到 GPU,但却能够不用任何像素数据而在 GPU 上完成所有的工作,类似的,对于渐变的图层,GPU 是能创建渐变的,而且不需要 CPU 做任何工作,并且不需要上传任何数据到 GPU。
如果一个 CALayer的子类实现了 -drawInContext: 或者它的代理,类似于 -drawLayer:inContest:,Core Animation 将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU 上,结果将会被上传到 GPU。
参考资源
l iOS-离屏渲染详解.
http://www.jianshu.com/p/57e2ec17585b
l 离屏渲染优化详解:实例示范+性能测试
http://www.jianshu.com/p/ca51c9d3575b
l 解决常见的masksToBounds离屏渲染带来的性能损耗
http://www.cocoachina.com/ios/20160315/15655.html
l iOS离屏渲染优化(附DEMO)http://www.cocoachina.com/ios/20160526/16457.html
l 绘制像素到屏幕上 https://objccn.io/issue-3-1/