Skip to content

Latest commit

 

History

History
107 lines (84 loc) · 8.15 KB

1.101.md

File metadata and controls

107 lines (84 loc) · 8.15 KB

离屏渲染

什么是离屏渲染

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的 Frame Buffer(帧缓冲区),作为像素数据存储区域,然后由显示控制器把帧缓冲区的数据显示到屏幕上。如果因为面临一些限制,比如阴影、光栅、遮罩等,CPU 无法把渲染结果直接写入 Frame Buffer,而是先暂时把中间的临时状态保存在额外的内存区域,之后再写入 Frame Buffer,那么这个过程被称为离屏渲染。 系统如果没有直接把渲染结果直接写入到 GPU FrameBuffer 中,则认为发生了一次离屏渲染。(离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作)

哪些 case 会触发离屏渲染

  • 光栅化(shouldRasterize) 属于系统的优化机制,当开启光栅化的时候,系统会将该图片以 BitMap 的形式缓存起来,缓存时间为100ms,当下次需要显示这张图片的时候,系统会从缓存中获取出这张图片传递给 GPU,不需要 GPU 再次渲染这部分图层,达到减少 GPU 运算量的目的。 应用场景非常小,因为时间仅为100ms,且会触发离屏渲染。
self.imageView.layer.shouldRasterize = YES
  • 遮罩(mask) 遮罩 Mask 相当于增加了 GPU 绘制复杂度,无法一次计算完成,需要增加一块新的帧缓冲区计算。
CALayer *layer = [CALayer layer];
layer.frame = // ...;
self.imageView.layer.mask = layer;
  • 阴影(shadow) 因为阴影位于视图下层,绘制完阴影,界面还没有绘制完主要内容,所以需要一块额外的帧缓冲区中转。
self.imageView.layer.shadowColor = [UIColor redColor].CGColor;
self.imageView.layer.shadowOpacity = 0.2;

如果给阴影设置 shadowPath 则不会触发离屏幕渲染。因为 shadowPath 预先告诉 CoreAnimation 框架阴影的几何形状,因此不需要依赖 layer 本体,可以独立渲染。

  • 抗锯齿(竖直图片旋转后会出现锯齿) 可能会触发离屏渲染,假如 UIImageView 控件的尺寸和图片素材的大小不一致,比如设置旋转,则会触发离屏渲染,否则不会。 抗锯齿的计算量很大,因此需要额外的帧缓冲区保存计算结果,则触发离屏渲染。
CGFloate angle = M_PI/60.0;
self.iamgeView.layer setTransform3DRotate(self.imageView.layer.transform, angle, 0, 0, 1);
self.iamgeView.layer.allowsEdgeAntialiasing = YES;
  • 不透明 当 alpha 为1的时候,不会触发离屏渲染。 当 alpha 不为1的时候,且设置了父视图的 allowsGroupOpacity 为 YES,则会触发离屏渲染。因为父视图的 allowsGroupOpacity 为 YES,则代表子视图的透明度是否和父视图一样,一样则需要额外的帧缓冲区计算。
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0, 10, 20)];
view.backgroundColor = [UIColor redColor];
[self.imageView addSubview: view];
self.iamgeView.alpha = 1;  
self.iamgeView.layer.allowsGroupOpacity = YES;
  • 圆角 当给视图设置了背景颜色且设置了圆角,则会触发离屏渲染。 UILabel 比较特殊,给 UILabel 设置 backgroundColor 其实就是给 UILabel 的 contents 设置背景颜色,contents 层级比 layer 高,所以 UILabel 整体显示为红色。 下面显示结果为:红绿
self.label.backgroundColor = [UIColor redColor];
self.label.layer.backgroundColor = [UIColor greenColor].CGColor;
self.label.layer.cornerRadius = YES;

self.iamgeView.backgroundColor = [UIColor redColor];
self.iamgeView.layer.backgroundColor = [UIColor greenColor].CGColor;
self.imageView.layer.cornerRadius = YES;

离屏渲染的影响?

离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。 上下文切换,不管是在GPU渲染过程中,还是广为人知的进程切换,上下文切换都是一个相当耗时的操作。首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。

一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次mask就是普通渲染的n(n>3)倍以上耗时操作

正常流程:App Source Code -> CPU -> Frame Buffer -> Dispaly 离屏渲染流程:App Source Code -> CPU -> Off Screen Frame Buffer -> Frame Buffer -> Dispaly

如何优化?

  • 针对 shadow 可以增加 shadowPath
  • 针对圆角可以增加贝塞尔曲线或者一张图片实现(类似遮罩)。

特殊的离屏渲染

  • drawRect 是一种特殊情况,因为是依赖 Core Graphics 将绘制结果保存在 backing store 中,是 CPU 层面的操作,离屏渲染是 GPU 层面的。
  1. 重写 drawRect 方法的时候系统会为该 View 创建一块内存区域,渲染结果先保存到该内存区域,最后将该内存区域的结果写入到 FrameBuffer 中,但是该现象不属于严格意义的离屏渲染。

画家算法

画家算法,也叫做优先填充算法,是计算机三维图形学中处理可见性问题的一种解法(三维场景投影到二维平面上)。画家算法先将场景中的多边形按照深度进行排序,然后按照由远及近的顺序进行描述,这样可以将不可见的部分覆盖,解决“可见性”问题(也就是一层层画布进行绘制,最后叠加)。

GPU 利用片元将整个图片分为一个个像素,并且并行计算了每一个像素的颜色。在同一个栅格内可能存在多个视图,根据距离眼睛的远近,存在多个不同的物体。显而易见,我们应该将最近物体的颜色作为该栅栏的颜色,后面物体的颜色应该被遮挡(如果后面物体的颜色被传递给片元着色器,这时候就是一个显示错误,比如我们打游戏的时候可以看到墙后的人)

画家算法带来2个问题。第一个问题是相互交错的物体(类似于死循环,无法pick出谁应该最先被渲染),按照画家算法,这样的情况,GPU 会无从下手。所以早期的时候,设计师总是避免这样相互交错的设计。 第二个问题是过度绘制,因为画家算法总是一层层绘制,所以存在重合叠加的情况,层级较低的物体总是会被过度绘制,浪费资源。

因为 GPU 的设计是并发、无序的,所以我们期望的画家算法是不希望浪费、等待,同时为了绘制速度,所以在此基础上引入了 Depth Buffer 和 Early-Z 和深度缓冲。

画家算法有个缺点,就是当后面的图层开始渲染时,是无法回过头去处理之前的图层,这就对于一些前后依赖的图层时,无法实现,因此需要申请一块额外的帧缓冲区来完成,比如阴影、圆角。

明白了画家算法的工作原理,也就明白了为什么会发生离屏渲染。

  • 离屏渲染需要创建额外的帧缓冲区
  • 渲染相关的上下文对象、帧缓冲区都比较大,切换会带来性能损耗
  • 内存拷贝,需要将临时帧缓冲区的内容拷贝到真正的帧缓冲区。单帧渲染都会比较耗费性能了,如果屏幕上多个视图渲染都存在离屏渲染,整个界面会发生卡顿。

如何检测离屏渲染

Xcode 就提供了检测功能,打开路径为: Xcode -> Debug -> View Debugging -> Rendering -> Color Offscreen-Renderer Yellow

引申阅读