离屏渲染优化详解:实例示范+性能测试

发表于:2018-4-04 14:55

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:seedante    来源:简书

  离屏渲染(Offscreen Render)
  objc.io 出品的 Getting Pixels onto the Screen 的翻译版绘制像素到屏幕上应该是国内对离屏渲染这个概念推广力度最大的一篇文章了。文章里提到「直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。」触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。
  官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。
  最初应该是从英文开发者那里传开的:使用 Core Graphics 里的绘制 API 也会触发离屏渲染,比如重写 drawRect:。为什么几年前会产生这样的认识不得而知。在 WWDC 2011: Understanding UIKit Rendering 这个 session 里演示了「Core Animation Instruments」里使用「Color Offscreen-Renderd Yellow」选项来检测离屏渲染,在 WWDC 2014: Advanced Graphics and Animations for iOS Apps 也专门演示了这个工具。
  Core Animation Instruments Debug Options
  Designing for iOS: Graphics & Performance 这篇文章也提到了使用 Core Graphics API 会触发离屏渲染,这引出了 Andy Matuschak,苹果 iOS 4.1-8 时期 UIKit 组成员 ,WWDC 2011: Understanding UIKit Rendering 主讲人之一,对这个观点的回复,主要意思是:「Core Graphics 的绘制 API 的确会触发离屏渲染,但不是那种 GPU 的离屏渲染。使用 Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 版本的离屏渲染。」
  本文以「Color Offscreen-Renderd Yellow」为触发离屏渲染的标准,除非还有这个标准无法检测出来的引发离屏渲染的行为。那么 Core Graphics API 是不会触发离屏渲染的,比如重写drawRect:,而除了以上四种效果会触发离屏渲染,使用系统提供的圆角效果也会触发离屏渲染,比如这样:
  view.layer.cornerRadius = 5
  view.layer.masksToBounds = true
  圆角优化前段时间在微博上刷了好一阵,不想凑热闹,不过这个话题必须讲一讲。
  开始之前,先铺垫一点基础的东西。
  UIView 和 CALayer 的关系
  The Relationship Between Layers and Views 的解释很细致但是太啰嗦,简单来说,UIView 是对 CALayer 的一个封装。
  出自 WWDC 2012: iOS App Performance: Graphics and Animations
  CALayer 负责显示内容contents,UIView 为其提供内容,以及负责处理触摸等事件,参与响应链。CALayer 的结构如下,出自 Layers Have Their Own Background and Border:
  CALayer 构成
  CALayer 有三个视觉元素,中间的contents属性是这样声明的:var contents: AnyObject?,实际上它必须是一个CGImage才能显示。
  当使用let view = UIView(frame: CGRectMake(0, 0, 200, 200))生成一个视图对象并添加到屏幕上时,从 CALayer 的结构可以知道,这个视图的 layer 的三个视觉元素是这样的:contents为空,背景颜色为空(透明色),前景框宽度为0的前景框,这个视图从视觉上看什么都看不到。CALayer 文档第一句话就是:「The CALayer class manages image-based content and allows you to perform animations on that content.」UIView 的显示内容很大程度上就是一张图片(CGImage)。
  UIImageView
  既然直接对 CALayer 的contents属性赋值一个CGImage便能显示图片,所以 UIImageView 就顺利成章地诞生了。实际上 UIImage 就是对 CGImage(或者 CIImage) 的一个轻量封装。记得我刚接触 iOS 时,搞不懂这两者的区别,有人这样对我说过,没想到出处是这里:
  出自 WWDC 2012: iOS App Performance: Graphics and Animations
  UIKit 和 Core Graphics 框架的联系很紧密,UIKit 里带CG前缀属性的类基本上是对应 Core Graphics 框架里的对象的封装,UIKit 里的绘制功能也是 Core Graphics 绘制 API 的封装。Drawing with Quartz and UIKit 列举了这些对应关系。界面的内容主要是图像和文字,文字是怎么显示的?也是使用 Core Graphics 框架绘制出来的。
  接下来,正式开始本文的话题。
  RoundedCorner
  设置圆角:
  view.layer.cornerRadius = 5
  这行代码做了什么?文档中cornerRadius属性的说明:
  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 YES causes the content to be clipped to the rounded corners.
  很明了,只对前景框和背景色起作用,再看 CALayer 的结构,如果contents有内容或者内容的背景不是透明的话,还需要把这部分弄个角出来,不然合成的结果还是没有圆角,所以才要修改masksToBounds为true(在 UIView 上对应的属性是clipsToBounds,在 IB 里对应的设置是「Clip Subiews」选项)。前些日子很热闹的圆角优化文章中的2篇指出是修改masksToBounds为true而非修改cornerRadius才是触发离屏渲染的原因,但如果以「Color Offscreen-Renderd Yellow」的特征为标准的话,这两个属性单独作用时都不是引发离屏渲染的原因,他俩合体(masksToBounds = true, cornerRadius>0)才是。
  系统圆角需要裁剪 layer 中间的contents,这其中裁剪工作和离屏渲染对性能的影响哪个占的比重大?我对此有点疑问。虽然系统圆角下裁剪工作和离屏渲染无法拆分,但可以单独测试出裁剪工作对性能的影响。我使用上面提到的某篇优化圆角的文章提供的 Demo 在快速滚动下得到的帧率如下,在此基础上验证测试:
  基础帧率
  图中括号内的数量代表滚动时同屏下圆角效果的个数。同时测试了圆角半径对性能的影响,两者没有关系,cornerRadius分别为0.1和10的时候无明显差别。使用「Color Offscreen-Renderd Yellow」来检测时,只有圆角部分才会有黄色特征,因此在cornerRadius = 0.1的时候基本观测不到,如果你对cornerRadius和masksToBounds合体才能触发离屏渲染有疑问,对比帧率就知道了。
  这个 Demo 里的优化方案是重绘圆角,作者给出了他在 iPhone 6 上的测试结果,非常好。奇怪的是 Demo 里没有将绘制圆角的工作放到后台,文章里没有对此进行解释,不过这个 Demo 在我服役多年的 iPad mini 1代(iOS 9.3.1)上的运行结果是无法让人满意的,显然应该放在后台重绘再切换到主线程设置内容。做个对比测试,前台圆角:主线程绘制圆角(Demo 的优化方法),后台圆角:将原 Demo 的绘制操作放到后台线程然后切换到主线程,同屏圆角数量为24个,对比结果:
  圆角对比
  前台圆角的性能稍好于系统圆角,后台圆角的表现和无圆角持平。经过测试,masksToBounds=true和cornerRadius>0在单独作用的时候对性能基本没有影响(针对无圆角,前台圆角和后台圆角),且单独作用下无法观察到离屏渲染时的黄色特征,也就是说只有系统圆角才触发了离屏渲染。
  对比上面的测试结果,眼看就要得出「在系统圆角中(阻塞主线程的)裁剪工作是影响性能的主要因素,黑锅不该离屏渲染来背。」的结论来了。视图性能出现问题时,要分清瓶颈是在 CPU 还是 GPU 上,使用 GPU Driver Instruments 来检测。以下测试中同屏圆角数量在24个左右:
  系统圆角
  系统圆角: 帧率很低,CPU 利用率较低,GPU 利用率很高
  前台圆角
  前台圆角:帧率比上面稍好,不稳定,CPU 利用率起伏很大,高峰接近100%,低谷在20%以下,GPU 利用率很低
  后台圆角
  后台圆角:帧率非常好,CPU 利用率起伏非常大,高峰超过120%,低谷在10%以下,GPU 利用率很低
  惨遭打脸!有点意外的是前台圆角的 CPU 使用率和后台圆角一样起伏都很大。重绘圆角时,绘制工作是由 CPU 完成的,这可能成为性能的瓶颈,在系统圆角下 GPU 是瓶颈,由于无法将离屏渲染和我所谓的裁剪工作分开,之前试图用自行绘制圆角妄图证明系统圆角里裁剪圆角的工作是影响性能主因的对比测试是没有意义的。
  Mastering UIKit Performance 里介绍离屏渲染时也举了圆角的例子,他给出的代码并没有在后台绘制圆角,另一方面他表示绘制圆角的代码只会执行一次(在实际使用时的确应该这样设计,只绘制一次,后续直接使用重绘的结果),但从贴出来的代码来看绘制代码无法只执行一次(毕竟是 Demo,没有优化这一点,实际上就变成了和系统圆角一样,滚动的每一帧都在重绘),这样一来就变成了在主线程进行手工绘制圆角,优化效率不高,而且从最后贴出的帧率截图来看并没有达到结论所说的那样高帧率以及稳定性。由于这篇文章并没有开发源代码,无法探明其中的差异。他的测试硬件是 iPhone 4(iOS 7.1.1),而我的 iPad mini 1代与 iPhone 4相差两年,上面的 Demo 里的测试硬件是 iPhone 6,又相差2年,考虑到硬件性能的差异,重绘圆角应该放到后台才是最优解。
  OffscreenRenderDemo
  还有其他的几个效果需要测试,所以还是要写个 Demo 的:OffscreenRenderDemo,里面包括本文涉及的所有效果演示以及优化方案。测试的 Demo 还是老一套,TableView 配合图像和文本,长这样,接下来的效果测试都主要集中在左侧的两个 UIImageView 上,尺寸都为(80, 80),cell 高度为100。
  界面元素
  测试环境为:
  iPad mini 1st generation with iOS 9.3.1
  Xcode 7.3 with Swift 2.2
  OS X 10.11.4
  在 Demo 里实现了圆角的优化,这个话题还没有结束呢,上一节只是证明了界面滚动过程中大量的离屏渲染确实是帧率杀手。再放图就特别占地方了,接下来就用表格来呈现数据,数据是我目测计算出来的,会有误差,而且是单次测试,但是量级是没有问题的。接下来的描述中:左右代表在某个值附近浮动,以下代表都接近某个值,很少有超过的,以上代表绝大部分在某个值以上,但超过幅度不大。
  OffscreenRenderDemo 的基准性能:
  CPU 的利用率很难用平均数值呈现,从上面也可以看到 CPU 的利用率是周期性的波动,这是这类 Demo 的特点,这导致很难对比两次测试中的 CPU 利用率。上面的表格里标注的波动范围仅能当作 CPU 是否是性能瓶颈的参考,而不能与其他测试进行对比。上面的图里 CPU 的采样间隔是1ms,FPS 和 GPU 的利用率的采样间隔是1s,这些是默认值。如果你希望增大 CPU 采样间隔时间来形成类似的柱状图,基本上没有意义,这里的数据是累计利用率,稍不注意看到的都超过100%。触发离屏渲染的效果的瓶颈主要是 GPU,CPU 的利用率偏低,当然,视图性能跟 CPU 和 GPU 都有关,后面的效果会对 CPU 的利用率做出说明。
  在我的 Demo 里,后台绘制圆角自不必说和无任何效果下的性能非常接近,在主线程绘制圆角的性能只是略微下降。这与上一个 Demo 的相关情况相差很大,上面的结果显示在同屏幕圆角数量24个的情况下,平均帧率勉强在40左右,修正为20个测试一次,平均帧率依然在40附近徘徊。我的 Demo 在主线程以及后台线程绘制圆角时 CPU 的利用率也不像上一个 Demo 那样变化剧烈。由于代码的差异,这些情况很难说明什么,但再次证明一点,为了高帧率,后台绘制才是最优解。
  大部分赚星星的方案都采用了重绘圆角,重绘的方式有多种,都是殊途同归。实际中重绘圆角的优化方案需要考虑的是,将图像重新绘制为为圆角图像相当于多了一份拷贝,要不要缓存?A.第一次重绘后将这些圆角图像缓存在磁盘里,第二次加载直接使用缓存的圆角图像;B.直接保存在内存里,在内存比较吃紧时显然不是个好选择;C.不缓存,和系统圆角一样,每次都重绘,浪费电量。
  说了这么多,重绘方案与其他的优化方案相比,并没有什么优势。来看看其他方案:
  1.如果不需要对外部来源的图片做圆角,由设计师直接画成圆角图片是最方便的;
  2.混合图层:在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。VVebo 微博客户端就是这样做的,遮挡的部分背景最好与周围背景相同。多一个图层会增加合成的工作量,但这点工作量与离屏渲染相比微不足道,性能上无论各方面都和无效果持平。下面左侧的图像是 VVebo 里用来制造圆形头像的 mask 图像,实际中有这种需求的基本是制造圆形头像,普通的圆角遮罩需要左二这种,左三是通用型。如果叠加的视图都一样,可以只加载一次遮罩图片以减少内存占用。
  遮罩
  除了用软件画出来保存在项目里,直接用代码画出来也是很简单的。即使不熟悉 Core Graphics 的 API,搜索出来的重绘圆角的代码看懂是很容易的,但要绘制出上面的图形还是有点棘手。这种事情多试试就好了:在一个设置opaque = false的 CGContext 里,设定填充颜色然后用两条贝塞尔曲线围成一个封闭区域,最后从这个绘制环境导出图像即可。我写了个函数来生成区域圆角遮罩图像:Draw a transparent image。
  如何在文本视图类上实现圆角?文本视图主要是这三类:UILabel, UITextField, UITextView。其中 UITextField 类自带圆角风格的外型,UILabel 和 UITextView 要想显示圆角需要表现出与周围不同的背景色才行。想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的contents呈现透明的背景色,文本视图类的 layer 的contents默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。不过 UILabel 上设置backgroundColor的行为被更改了,不再是设定 layer 的背景色而是为contents设置背景色,UITextView 则没有改变这一点,所以在 UILabel 上实现圆角要这么做:
  //不要这么做:label.backgroundColor = aColor 以及不要在 IB 里为 label 设置背景色
  label.layer.backgroundColor = aColor
  label.layer.cornerRadius = 5
  Shadow
  Shadow Properties 展示了阴影是如何与视图本身结合的:
  Layer displaying the shadow properties
  阴影直接合成在视图的下面,视图结构里并没有多出一个视图。在没有指定阴影路径时,阴影是沿着视图的非透明部分扩展的,而且 CALayer 的三个视觉元素至少有一个存在时才会有阴影。
  使用阴影必须保证 layer 的masksToBounds = false,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常这样实现一个阴影:
  let imageViewLayer = avatorView.layer
  imageViewLayer.shadowColor = UIColor.blackColor().CGColor
  imageViewLayer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示
  imageViewLayer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响
  imageViewLayer.shadowOffset = CGSize(width: 5, height: 5)
  //设定路径:与视图的边界相同
  let path = UIBezierPath(rect: cell.imageView.bounds)
  imageViewLayer.shadowPath = path.CGPath//路径默认为 nil
  在 OffscreenRenderDemo 里,仅开启阴影(没有指定路径,同屏数量10个以上)在滚动时帧率会大幅下降,检测到离屏渲染的黄色特征;指定一个与边界相同的简单路径后离屏渲染特征消失,帧率恢复正常。



上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号