显示器的电子束会从屏幕的左上角开始逐行扫描,屏幕上的每个点的图像信息都从帧缓冲器中的位图进行读取,在屏幕上对应地显示。扫描的流程如下图所示:
成都创新互联-专业网站定制、快速模板网站建设、高性价比周村网站开发、企业建站全套包干低至880元,成熟完善的模板库,直接使用。一站式周村网站制作公司更省心,省钱,快速模板网站建设找我们,业务覆盖周村地区。费用合理售后完善,十余年实体公司更值得信赖。
电子束扫描的过程中,屏幕就能呈现出对应的结果,每次整个屏幕被扫描完一次后,就相当于呈现了一帧完整的图像。屏幕不断地刷新,不停呈现新的帧,就能呈现出连续的影像。而这个屏幕刷新的频率,就是帧率(Frame per Second,FPS)。由于人眼的视觉暂留效应,当屏幕刷新频率足够高时(FPS 通常是 50 到 60 左右),就能让画面看起来是连续而流畅的。对于 iOS 而言,app 应该尽量保证 60 FPS 才是最好的体验。
在这种单一缓存的模式下,最理想的情况就是一个流畅的流水线:每次电子束从头开始新的一帧的扫描时,CPU+GPU 对于该帧的渲染流程已经结束,渲染好的位图已经放入帧缓冲器中。但这种完美的情况是非常脆弱的,很容易产生屏幕撕裂:
CPU+GPU 的渲染流程是一个非常耗时的过程。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 ---- 那么已扫描的部分就是上一帧的画面,而未扫描的部分则会显示新的一帧图像,这就造成屏幕撕裂。
解决屏幕撕裂、提高显示效率的一个策略就是使用垂直同步信号 Vsync 与双缓冲机制 Double Buffering。根据苹果的官方文档描述,iOS 设备会始终使用 Vsync + Double Buffering 的策略。
垂直同步信号(vertical synchronisation,Vsync)相当于给帧缓冲器加锁:当电子束完成一帧的扫描,将要从头开始扫描时,就会发出一个垂直同步信号。只有当视频控制器接收到 Vsync 之后,才会将帧缓冲器中的位图更新为下一帧,这样就能保证每次显示的都是同一帧的画面,因而避免了屏幕撕裂。
但是这种情况下,视频控制器在接受到 Vsync 之后,就要将下一帧的位图传入,这意味着整个 CPU+GPU 的渲染流程都要在一瞬间完成,这是明显不现实的。所以双缓冲机制会增加一个新的备用缓冲器(back buffer)。渲染结果会预先保存在 back buffer 中,在接收到 Vsync 信号的时候,视频控制器会将 back buffer 中的内容置换到 frame buffer 中,此时就能保证置换操作几乎在一瞬间完成(实际上是交换了内存地址)。
启用 Vsync 信号以及双缓冲机制之后,能够解决屏幕撕裂的问题,但是会引入新的问题:掉帧。如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况。
如图所示,A、B 代表两个帧缓冲器,当 B 没有渲染完毕时就接收到了 Vsync 信号,所以屏幕只能再显示相同帧 A,这就发生了第一次的掉帧。
手机使用卡顿的直接原因,就是掉帧。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。
这样看来,可以大概总结一下
屏幕卡顿的根本原因:
离屏渲染是指GPU在当前屏幕缓冲区意外新开辟一个缓冲区进行渲染操作。
简化来看,通常的渲染流程如下:
App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容。
而离屏渲染的流程是这样的:
从上面的流程来看,离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)。
并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。与此同时,Offscreen Buffer 的总大小也有限,不能超过屏幕总像素的 2.5 倍。
可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以大部分情况下,我们都应该尽量避免离屏渲染。
那么为什么要使用离屏渲染呢?主要是因为下面这两种原因:
对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。
SORRY 未完待续
在 iOS离屏渲染 介绍了离屏渲染的逻辑和原理,我们知道离屏渲染对于性能会有较大的消耗,那么开发中怎么避免产生离屏渲染或者优化离屏渲染呢?
Color Off-screen Rendered 开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
所以不裁剪子视图设置圆角的情况下是不会产生离屏渲染的
在iOS9之后苹果系统对圆角进行了优化,UIImageView和UILabel即使使用了layer.masksToBounds = YES;仍然不会产生离屏渲染,但是UIButton如果使用layer.masksToBounds = YES剪切子视图仍然会产生离屏渲染
②. 对图片进行提前切圆角的操作:
需要注意的是对图片切圆角时,最终的圆角是可能有缩放的,比如图片的尺寸imgSize为(100,200)而UIImageView的尺寸为(50,50)圆角尺寸如果为10,那么最终的效果是(100,200)的图片圆角为10,显示到UIImageView上x轴方向缩放2倍,y轴方向缩放4倍,所以这种方案缺点比较明显。
③. YYImage的圆角处理方案:
YYImage本质和第一种方案相同,都是对image的切圆角操作
④. 让UI切一个中间透明的镂空图片覆盖在UIImageView上
图层的叠加绘制遵循“画家算法”。
油画算法:先绘制场景中的离观察者较远的物体,再绘制较近的物体。
本来我们从后往前绘制,绘制完一个图层就可以丢弃了。但现在需要依次在 Offscreen Buffer中保存,等待圆角+裁剪处理,即引发了 离屏渲染
关于圆角,iOS 9及之后的系统版本,苹果进行了一些优化。
只设置contents或者UIImageView的image,并加上圆角+裁剪,不会产生离屏渲染。但加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
阴影会作用在所有子layer所组成的形状上,需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。那就只能等全部子layer画完才能得到。
但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去
离屏渲染的代价是很高的,主要体现在两个方面:
1 . 创建新缓冲区
想进行离屏渲染,首先要创建一个新的缓冲区,消耗内存。
特殊产品需求,为实现一些特殊动效果,需要多图层以及离屏缓存区保存中间状态,这种情况下就不得不使用离屏渲染。比如产品需要实现高斯模糊,无论自定义高斯模糊还是调用系统API都会触发离屏渲染。
一般都是系统自动触发的,比如阴影、圆角、模糊特效等等
虽然离屏渲染会需要多开辟出新的临时缓存区来存储中间状态,但是对于多次出现在屏幕上的数据,可以提前渲染好,从而进行复用,这样CPU/GPU就不用做一些重复的计算。
参考资料:
iOS 离屏渲染探究
关于iOS离屏渲染的深入研究
iOS圆角的离屏渲染,你真的弄明白了吗