iOS-什么是离屏渲染?

图片是如何显示在屏幕中的?

在iOS开发中,我们使用UIImage(model)获取图片数据,常见的图片格式包括.png和.jpeg等,其次利用UIKit中的UIImageView,将UIImage设置到UIImageView.image中。这个过程我们再熟悉不过了。

UIImage -> UIImageView -> Display

实际上,一个jpeg图片显示在屏幕上需要经历三个过程,第一步,请求jpeg图片数据data(来自磁盘或网络),加载至内存中,即data buffer;第二步,对data buffer进行解码,解码后的数据称为位图,即image buffer;第三步,将image buffer传递给GPU的frame buffer,由GPU将图片内容显示在屏幕中。

Buffers

Buffers 是一段连续的内存区域。


Buffers

Data Buffers

Data Buffers 存储图片文件(Image file:star.jpeg)的元数据。它的大小和图片存储在磁盘中文件大小一致,但其数据并不直接描述像素。


Data Buffers

Image Buffer

Image Buffers 代表图片元数据Data Buffers在内存中被解码后的表示,每个元素代表一个像素点的颜色,即常说的位图。其大小与Data Buffers大小成正比例。

一般来说,图片的色彩空间是 sRGB,即每个像素占四个字节,所以Image Buffer Size = Data Buffers Size * 4。

Image Buffers

Frame Buffer

Frame Buffer 存储了 App 的每帧的实际渲染输出(actual rendered output)。GPU会根据Frame Buffer 的内容按一定帧率显示在屏幕上。

因此,我们可以得到这样的一个图片渲染流程:


data(.jpeg) -> data buffer -> image buffer -> frame buffer

什么是离屏渲染?

了解完图片渲染流程后,下面开始介绍离屏渲染。那什么是离屏渲染呢?

当image buffer需要进行一些额外处理(如圆角、毛玻璃或其他滤镜)并且进行额外处理后无法直接将数据传递至frame buffer进行显示,需要将处理后的数据暂存至offscreen buffer中,再由offscreen buffer传递至frame buffer,最终显示在屏幕上,这个过程就称为离屏渲染。

offscreen buffer同为内存中的一块连续区域。在对图片进行额外处理时用于存放中间合成数据的区域。

因此,不一定执行圆角操作(额外处理)就一定会触发离屏渲染,还需要image buffer暂存至offscreen buffer这一过程。

综上,离屏渲染触发条件有两个:

  1. 图片(图层)需要额外处理
  2. 数据需要暂存至offscreen buffer

下面我们通过代码进行验证。

如何查看app中哪些视图发生了离屏渲染?

在iphone模拟器debug菜单中勾选Color off-screen Rendered即可。

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_border_background

从上图我们可以看到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的注意事项

  1. shouldRasterize会必然产生一次离屏渲染,因为开启了新内存空间来复用结果。
  2. layer的内容(包括子layer)必须是静态的,layer非静态意味着需要重新渲染,那么缓存就会失效,每一帧都开辟新内存区域即离屏渲染,这正是渲染流水线中极力避免的。我们可以利用xcode中的“Color Hits Green and Misses Red”的选项,查看缓存的使用是否符合预期。
  3. 缓存大小限制 <= 屏幕总像素的2.5倍
  4. 缓存有效期 <= 100ms,超过100ms未被使用则视为失效,从而丢弃。

shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了。


离屏渲染的常见场景

  1. 圆角cornerRadius+clipsToBounds

  2. 阴影shadow

  3. 组透明度group opacity

  4. 遮罩mask

  5. 毛玻璃效果UIBlurEffect


  6. 其他还有一些,如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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341