图片是如何显示在屏幕中的?
在iOS开发中,我们使用UIImage(model)获取图片数据,常见的图片格式包括.png和.jpeg等,其次利用UIKit中的UIImageView,将UIImage设置到UIImageView.image中。这个过程我们再熟悉不过了。
实际上,一个jpeg图片显示在屏幕上需要经历三个过程,第一步,请求jpeg图片数据data(来自磁盘或网络),加载至内存中,即data buffer;第二步,对data buffer进行解码,解码后的数据称为位图,即image buffer;第三步,将image buffer传递给GPU的frame buffer,由GPU将图片内容显示在屏幕中。
Buffers
Buffers 是一段连续的内存区域。
Data Buffers
Data Buffers 存储图片文件(Image file:star.jpeg)的元数据。它的大小和图片存储在磁盘中文件大小一致,但其数据并不直接描述像素。
Image Buffer
Image Buffers 代表图片元数据Data Buffers在内存中被解码后的表示,每个元素代表一个像素点的颜色,即常说的位图。其大小与Data Buffers大小成正比例。
一般来说,图片的色彩空间是 sRGB,即每个像素占四个字节,所以Image Buffer Size = Data Buffers Size * 4。
Frame Buffer
Frame Buffer 存储了 App 的每帧的实际渲染输出(actual rendered output)。GPU会根据Frame Buffer 的内容按一定帧率显示在屏幕上。
因此,我们可以得到这样的一个图片渲染流程:
什么是离屏渲染?
了解完图片渲染流程后,下面开始介绍离屏渲染。那什么是离屏渲染呢?
当image buffer需要进行一些额外处理(如圆角、毛玻璃或其他滤镜)并且进行额外处理后无法直接将数据传递至frame buffer进行显示,需要将处理后的数据暂存至offscreen buffer中,再由offscreen buffer传递至frame buffer,最终显示在屏幕上,这个过程就称为离屏渲染。
offscreen buffer同为内存中的一块连续区域。在对图片进行额外处理时用于存放中间合成数据的区域。
因此,不一定执行圆角操作(额外处理)就一定会触发离屏渲染,还需要image buffer暂存至offscreen buffer这一过程。
综上,离屏渲染触发条件有两个:
- 图片(图层)需要额外处理
- 数据需要暂存至offscreen buffer
下面我们通过代码进行验证。
如何查看app中哪些视图发生了离屏渲染?
在iphone模拟器debug菜单中勾选Color off-screen Rendered即可。
上图中黄色区域即是发生了离屏渲染的区域。可以看到只有第一个UIImageView的四周圆角区域触发了离屏渲染,第二个和第三个UIImageView的四周圆角区域并没有触发了离屏渲染。
因为只有第一个UIImageView的设置同时满足了触发离屏渲染的两个条件,第二个和第三个都只满足离屏渲染的第一个条件,即通过设置圆角触发了对图像的额外处理,没有满足第二个条件,image buffer暂存至offscreen buffer。
那什么时候会触发第二个条件呢?下面我们开始说明离屏渲染的本质。
离屏渲染的本质
在这之前我们需要了解渲染中的常用算法:油画算法。
油画算法(摘自关于iOS离屏渲染的深入研究)
渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销)。
然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。
offscreen buffer
从上面的例子中我们知道如果图片进行额外处理时导致image buffer暂存至offscreen buffer,那么就会触发离屏渲染。可以理解为,图像额外处理过程较复杂,渲染流水线无法找到单次遍历就能完成渲染的算法,需要暂存中间数据至offscreen buffer,待所有操作处理完成后再传递至frame buffer。
即触发数据需要暂存至offscreen buffer的条件是:渲染流水线无法找到单次遍历就能完成渲染的算法,需要开辟新的内存区域(offscreen buffer)保存中间值。,如UIImageView.image+cornerRadius+masksToBounds。
从上图我们可以看到layer中包括,backgroundColor(背景图)、contents(image,我们想要显示的图片)和border(边框)。三者叠加后最终显示到屏幕上。
下面我们对之前的三个UIImageView触发离屏渲染的情况进行分析,
第一个UIImageView的设置:
//1.UIImageView 设置图片+背景色;
imgView1.image = image;// contents
imgView1.backgroundColor = [UIColor systemTealColor];// backgroundColor
imgView1.layer.cornerRadius = 50;
imgView1.layer.masksToBounds = YES;
即UIImageView的layer有backgroundColor和contents,masksToBounds = YES后,contents会执行圆角操作,因此,backgroundColor和contents都需要执行圆角操作,之后进行叠加合并最终显示到屏幕上。这个过程中存在多个处理操作,渲染流水线无法找到单次遍历就能完成渲染的算法,因此数据无法直接传递frame buffer从而触发离屏渲染。从图中可以看出只有UIImageView的四个顶点区域发生了离屏渲染。
第二个UIImageView的设置:
//2.UIImageView 只设置图片,无背景色;
imgView2.image = image;// contents
imgView2.layer.cornerRadius = 50;
imgView2.layer.masksToBounds = YES;
即UIImageView的layer只有contents,masksToBounds = YES后,contents会执行圆角操作,最后显示到屏幕上。这个过程中存在渲染流水线单次遍历就能完成渲染的算法,因此数据直接传递frame buffer,避免了offscreen buffer的使用,从而没有触发离屏渲染。
第三个UIImageView的设置:
//3.UIImageView 仅设置背景色,无图片;
imgView3.backgroundColor = [UIColor systemTealColor];// backgroundColor
imgView3.layer.cornerRadius = 50;
imgView3.layer.masksToBounds = YES;// yes or no均不影响结果
即UIImageView的layer只有backgroundColor,设置cornerRadius后backgroundColor会执行圆角操作,最后显示到屏幕上。这个过程中存在渲染流水线单次遍历就能完成渲染的算法,因此数据直接传递frame buffer,避免了offscreen buffer的使用,从而没有触发离屏渲染。
离屏渲染的坏处
图形在屏幕中以60hz或更高频率进行显示而不产生掉帧的前提是CPU和GPU高效协同,GPU的操作是高度流水线化的。
渲染流水线的所有计算工作都在有条不紊地正在向frame buffer输出,此时中断流水线,丢弃掉之前已完成的计算工作,开辟新的内存区域offscreen buffer,完成只服务于我们的“切圆角”操作,完成后再传递至frame buffer,切换回到向frame buffer输出的正常流程。
-
切换上下文、开辟新内存都是耗时的CPU操作,CPU 占用越高,耗电越快,响应速度越慢。
-
如果性能损耗负担过大,如在tableView或者collectionView中,滚动的每一帧变化都会触发每个cell的重新绘制,16ms(60hz)内无法完成渲染则会导致掉帧。
如何高效地使用离屏渲染
除了尽量避免离屏渲染外,势必不可避免的有离屏渲染发生的场景,那么如何高效地使用离屏渲染呢?
答案是栅格化,在CALayer中有一个shouldRasterize属性,开启后layer会启动栅格化。
好处是通过开辟新内存区域缓存位图,提高性能。我们可以从官方文档中看到说明:
/* When true, the layer is rendered as a bitmap in its local coordinate
* space ("rasterized"), then the bitmap is composited into the
* destination (with the minificationFilter and magnificationFilter
* properties of the layer applied if the bitmap needs scaling).
* Rasterization occurs after the layer's filters and shadow effects
* are applied, but before the opacity modulation. As an implementation
* detail the rendering engine may attempt to cache and reuse the
* bitmap from one frame to the next. (Whether it does or not will have
* no affect on the rendered output.)
*
* When false the layer is composited directly into the destination
* whenever possible (however, certain features of the compositing
* model may force rasterization, e.g. adding filters).
*
* Defaults to NO. Animatable. */
@property BOOL shouldRasterize;
启用shouldRasterize的注意事项
- shouldRasterize会必然产生一次离屏渲染,因为开启了新内存空间来复用结果。
- layer的内容(包括子layer)必须是静态的,layer非静态意味着需要重新渲染,那么缓存就会失效,每一帧都开辟新内存区域即离屏渲染,这正是渲染流水线中极力避免的。我们可以利用xcode中的“Color Hits Green and Misses Red”的选项,查看缓存的使用是否符合预期。
- 缓存大小限制 <= 屏幕总像素的2.5倍
- 缓存有效期 <= 100ms,超过100ms未被使用则视为失效,从而丢弃。
shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了。
离屏渲染的常见场景
圆角cornerRadius+clipsToBounds
阴影shadow
组透明度group opacity
遮罩mask
-
毛玻璃效果UIBlurEffect
其他还有一些,如allowsEdgeAntialiasing,原理也都是类似:如果你无法仅仅使用frame buffer来画出最终结果,那就只能另开一块内存空间来储存中间结果。
圆角实现优化
Corner Rounding
圆角实现方案
[iOS] 图像处理 - 一种高效裁剪图片圆角的算法
参考
Image and Graphics Best Practices
Image and Graphics Best Practices,总结及延伸
关于iOS离屏渲染的深入研究
iOS 视图、动画渲染机制探究
Getting Pixels onto the Screen
advanced_graphics_and_animation_performance