iOS屏幕撕裂、屏幕卡顿、离屏渲染的相关探究

这篇文章我们来探究下屏幕撕裂、屏幕卡顿、离屏渲染。

一、屏幕撕裂

图片来源于网络

在探究屏幕撕裂问题之前,我们需要先了解下屏幕显示图像的原理。
图片来源于网络

电子枪按照上图显示的那样,从上往下逐行扫描,扫描完成后,就显示一帧的画面,随后电子枪回到初始位置。当电子枪换到新的一行时,会发出水平同步信号(Horizonal Synchronization),简称Hsync。而当一帧显示完成后,电子枪回到初始位置,准备画下一帧之前,显示器会发出一个垂直同步信号(Vertical Synchronization),简称VSync。显示器通常以固定频率进行刷新,这个刷新率就是VSync信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
image.png

从上图可以看出,如果帧缓存区只有一个,这时帧缓存区的读取和刷新都都会有比较大的效率问题。因此引入了双缓存机制。在这种情况下,GPU会预先渲染好一帧放入一个缓冲区内,让显示控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

双缓存机制虽然能解决效率问题,但是又带来了一个新的问题。

显示控制器帧缓存区进行读取图像进行显示时,如果当前这一帧的内容还未读取完成,GPU又将新的一帧内容提交到帧缓冲区并把两个帧缓冲区进行更新后,显示控制器就会把新的一帧数据的下半段显示到屏幕上,就会造成屏幕撕裂的现象。

屏幕撕裂产生的原因

屏幕撕裂就是在于显卡输出帧的速度比显示器快,显示器的处理速度跟不上显卡,在显示器处理显卡丢过来的第1帧的时候,第2帧就又到了,导致同一个画面同时出现1、2两帧,屏幕撕裂就产生了。

如何解决屏幕撕裂

在双缓存基础上,引入垂直同步信号。

垂直同步信号

开启垂直同步后,显卡绘制3D图形前会等待垂直同步信号,当该信号到达时,显卡才开始绘制3D图形,如果显卡性能较为强劲,在下个垂直同步信号到来之前已经完成了对该帧的渲染,显卡就会暂停处理,等下个垂直同步信号到来后才开始渲染下一帧。通俗的来讲,垂直同步就是让显卡每秒输出的帧数等于显示器的刷新率垂直同步是用来防止画面撕裂的,反之,关闭垂直同步就会出现撕裂、跳帧的情况。

垂直同步信号虽然能解决屏幕撕裂现象,也增加了画面流畅度,但是需要消费更多的计算资源,也会带来部分延迟(屏幕卡顿)。

二、屏幕卡顿

图片来源于网络

VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在 CPU中计算显示内容,比如视图的创建布局计算图片解码文本绘制等。随后CPU会将计算好的内容提交到GPU去,由GPU进行变换合成渲染。随后GPU会把渲染结果提交到帧缓冲区去,等待下一次VSync信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个VSync时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是屏幕卡顿的原因。

从上面的图中可以看到,CPUGPU不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对CPUGPU压力进行评估和优化。

垂直同步信号与双缓冲的意义

强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。

屏幕卡顿的本质

CPUGPU渲染流水线耗时过长,导致掉帧。

三、离屏渲染

APP通常的渲染流程:

image.png

App通过CPUGPU的合作,不停地将内容渲染完成放入Framebuffer帧缓冲器中,而显示屏幕不断地从Framebuffer中获取内容,显示实时的内容。

而离屏渲染的流程:
image.png

与普通情况下GPU直接将渲染好的内容放入Framebuffer中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将Offscreen Buffer中的内容进一步叠加渲染,完成后将结果切换到Framebuffer中。

离屏渲染带来的问题

离屏渲染时由于App需要提前对部分内容进行额外的渲染并保存到Offscreen Buffer,以及需要在必要时刻对Offscreen BufferFramebuffer进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)。

并且Offscreen Buffer本身就需要额外的空间,大量的离屏渲染会造成内存过大的压力。与此同时,Offscreen Buffer的总大小也有限,不能超过屏幕总像素的2.5倍。

可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。

为什么需要离屏渲染

既然离屏渲染会带来那么多的问题,那为什么又需要离屏渲染呢?

  • 一些特殊效果需要使用额外的Offscreen Buffer来保存渲染的中间状态,所以不得不使用离屏渲染。比如系统自动触发的阴影、圆角等。
  • 出于效率的目的,可以将内容提前渲染保存在Offscreen Buffer中,达到复用的目的。
光栅化(shouldRasterize )

开启光栅化后,会触发离屏渲染Render Server会强制将CALayer的渲染位图结果bitmap保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率

而保存的bitmap包含layersubLayer、圆角阴影组透明度 group opacity等,所以如果layer的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。

圆角阴影组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。

不过使用光栅化的时候需要注意以下几点:

  • 如果layer不能被复用,则没有必要打开光栅化
  • 如果layer不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
    离屏渲染缓存内容有时间限制,缓存内容100ms内如果没有被使用,那么就会被丢弃,无法进行复用
  • 离屏渲染缓存空间有限,超过2.5倍屏幕像素大小的话也会失效,无法复用
圆角的离屏渲染

图片来源于网络

layer由三层组成,我们设置圆角,会这样做:

view.layer.cornerRadius = 2

而苹果文档指出,cornerRadius只会默认设置backgroundColorborder的圆角,而不会设置content的圆角,除非同时设置了layer.masksToBoundstrue(对应UIViewclipsToBounds属性)

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

因此我们就知道,如果只是设置了cornerRadius而没有设置masksToBounds,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于masksToBounds会对 layer以及所有subLayercontent都进行裁剪,所以会触发离屏渲染

离屏渲染的逻辑

图层的叠加绘制大概遵循画家算法,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分。

图片来源于网络

在普通的layer绘制中,上层的sublayer会覆盖下层的sublayer,下层sublayer绘制完之后就可以抛弃了,从而节约空间提高效率。所有sublayer依次绘制完毕之后,整个绘制过程完成,就可以进行后续的呈现了。
图片来源于网络

而当我们设置了cornerRadius以及masksToBounds进行圆角 + 裁剪时,如前文所述masksToBounds裁剪属性会应用到所有sublayer上。这也就意味着所有sublayer必须要重新被应用一次圆角+裁剪,这也就意味着所有sublayer第一次被绘制完之后,并不能立刻被丢弃,而必须要被保存在 Offscreen buffer等待下一轮圆角+裁剪,这也就诱发了离屏渲染
图片来源于网络

实际上不只是圆角+裁剪,如果设置了透明度+组透明(layer.allowsGroupOpacity+layer.opacity),阴影属性(shadowOffset)等,都会产生类似的效果,因为组透明度阴影都是和裁剪类似的,会作用与layer以及其所有sublayer上,这就导致必然会引起离屏渲染

避免圆角离屏渲染

除了尽量减少圆角裁剪的使用,还有什么别的办法可以避免圆角+裁剪引起的离屏渲染吗?

由于刚才我们提到,圆角引起离屏渲染的本质是裁剪的叠加,导致masksToBoundslayer以及所有sublayer进行二次处理。那么我们只要避免使用masksToBounds进行二次处理,而是对所有的sublayer进行预处理,就可以只进行画家算法,用一次叠加就完成绘制。

那么可行的实现方法大概有下面几种:

  • 直接使用带圆角的图片,或者替换背景色为带圆角的纯色背景图,从而避免使用圆角裁剪。不过这种方法需要依赖具体情况,并不通用。
  • 再增加一个和背景色相同的遮罩mask覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
  • 贝塞尔曲线绘制闭合带圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的layer渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是layer的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对framecolor等进行手动地监听并重绘。
  • 重写drawRect,用CoreGraphics相关方法,在需要应用圆角时进行手动绘制。不过CoreGraphics效率也很有限,如果需要多次调用也会有效率问题。
触发离屏渲染原因的总结
  • 使用了masklayer(layer.mask)
  • 需要进行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
  • 设置了组透明度YES,并且透明度不为1layer(layer.allowsGroupOpacity/layer.opacity)
  • 添加了投影的layer(layer.shadow)
  • 采用了光栅化的layer(layer.shouldRasterize)
  • 绘制了文字的layer(UILabel, CATextLayer, Core Text 等)
总结一下:设置圆角不一定会导致离屏渲染,离屏渲染不一定是由于设置圆角产生的

参考:iOS 渲染原理解析

你可能感兴趣的:(iOS屏幕撕裂、屏幕卡顿、离屏渲染的相关探究)