离屏渲染优化详解:实例示范+性能测试
<h2><strong>离屏渲染(Offscreen Render)</strong></h2> <p>objc.io 出品的 <a href="/misc/goto?guid=4959714507916325582" rel="nofollow,noindex">Getting Pixels onto the Screen</a> 的翻译版 <a href="/misc/goto?guid=4959663557869314082" rel="nofollow,noindex">绘制像素到屏幕上</a> 应该是国内对离屏渲染这个概念推广力度最大的一篇文章了。文章里提到「直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。」触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。</p> <p>官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。</p> <p>最初应该是从英文开发者那里传开的:使用 Core Graphics 里的绘制 API 也会触发离屏渲染,比如重写 drawRect: 。为什么几年前会产生这样的认识不得而知。在 <a href="/misc/goto?guid=4959714508037942899" rel="nofollow,noindex">WWDC 2011: Understanding UIKit Rendering</a> 这个 session 里演示了「Core Animation Instruments」里使用「Color Offscreen-Renderd Yellow」选项来检测离屏渲染,在 <a href="/misc/goto?guid=4959714508116982977" rel="nofollow,noindex">WWDC 2014: Advanced Graphics and Animations for iOS Apps</a> 也专门演示了这个工具。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/511a0c4011646115d39379ffa805a5a9.png"></p> <p style="text-align:center">Core Animation Instruments Debug Options</p> <p><a href="/misc/goto?guid=4959714508211174222" rel="nofollow,noindex">Designing for iOS: Graphics & Performance</a> 这篇文章也提到了使用 Core Graphics API 会触发离屏渲染,这引出了 Andy Matuschak,苹果 iOS 4.1-8 时期 UIKit 组成员 , <a href="/misc/goto?guid=4959714508037942899" rel="nofollow,noindex">WWDC 2011: Understanding UIKit Rendering</a> 主讲人之一,对这个观点的 <a href="/misc/goto?guid=4959714508305142792" rel="nofollow,noindex">回复</a> ,主要意思是:「Core Graphics 的绘制 API 的确会触发离屏渲染,但不是那种 GPU 的离屏渲染。使用 Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 版本的离屏渲染。」</p> <p>本文以「Color Offscreen-Renderd Yellow」为触发离屏渲染的标准,除非还有这个标准无法检测出来的引发离屏渲染的行为。那么 Core Graphics API 是不会触发离屏渲染的,比如重写 drawRect: ,而除了以上四种效果会触发离屏渲染,使用系统提供的圆角效果也会触发离屏渲染,比如这样:</p> <pre> <code class="language-objectivec">view.layer.cornerRadius = 5 view.layer.masksToBounds = true </code></pre> <p>圆角优化前段时间在微博上刷了好一阵,不想凑热闹,不过这个话题必须讲一讲。</p> <p>开始之前,先铺垫一点基础的东西。</p> <h2><strong>UIView 和 CALayer 的关系</strong></h2> <p><a href="/misc/goto?guid=4959714508386718183" rel="nofollow,noindex">The Relationship Between Layers and Views</a> 的解释很细致但是太啰嗦,简单来说,UIView 是对 CALayer 的一个封装。</p> <p><img src="https://simg.open-open.com/show/0119d9d507afc80d64047364d8fc1f2c.png"></p> <p>出自 WWDC 2012: iOS App Performance: Graphics and Animations</p> <p>CALayer 负责显示内容 contents ,UIView 为其提供内容,以及负责处理触摸等事件,参与响应链。CALayer 的结构如下,出自 <a href="/misc/goto?guid=4959714508469408527" rel="nofollow,noindex">Layers Have Their Own Background and Border</a> :</p> <p><img src="https://simg.open-open.com/show/837a767e87c478db278d49fefac68424.png"></p> <p>CALayer 构成</p> <p>CALayer 有三个视觉元素,中间的 contents 属性是这样声明的: var contents: AnyObject? ,实际上它必须是一个 CGImage 才能显示。</p> <p>当使用 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)。</p> <h2><strong>UIImageView</strong></h2> <p>既然直接对 CALayer 的 contents 属性赋值一个 CGImage 便能显示图片,所以 UIImageView 就顺利成章地诞生了。实际上 UIImage 就是对 CGImage(或者 CIImage) 的一个轻量封装。记得我刚接触 iOS 时,搞不懂这两者的区别,有人这样对我说过,没想到出处是这里:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/335759bf376349ddfae4bbbb12431688.png"></p> <p style="text-align:center">出自 WWDC 2012: iOS App Performance: Graphics and Animations</p> <p>UIKit 和 Core Graphics 框架的联系很紧密,UIKit 里带 CG 前缀属性的类基本上是对应 Core Graphics 框架里的对象的封装,UIKit 里的绘制功能也是 Core Graphics 绘制 API 的封装。 <a href="/misc/goto?guid=4959714508551018752" rel="nofollow,noindex">Drawing with Quartz and UIKit</a> 列举了这些对应关系。界面的内容主要是图像和文字,文字是怎么显示的?也是使用 Core Graphics 框架绘制出来的。</p> <p>接下来,正式开始本文的话题。</p> <h2><strong>RoundedCorner</strong></h2> <p>设置圆角:</p> <pre> <code class="language-objectivec">view.layer.cornerRadius = 5 </code></pre> <p>这行代码做了什么?文档中 cornerRadius 属性的说明:</p> <p>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.</p> <p>很明了,只对前景框和背景色起作用,再看 CALayer 的结构,如果 contents 有内容或者内容的背景不是透明的话,还需要把这部分弄个角出来,不然合成的结果还是没有圆角,所以才要修改 masksToBounds 为 true (在 UIView 上对应的属性是 clipsToBounds ,在 IB 里对应的设置是「Clip Subiews」选项)。前些日子很热闹的圆角优化文章中的2篇指出是修改 masksToBounds 为 true 而非修改 cornerRadius 才是触发离屏渲染的原因,但如果以「Color Offscreen-Renderd Yellow」的特征为标准的话,这两个属性单独作用时都不是引发离屏渲染的原因,他俩合体( masksToBounds = true, cornerRadius>0 )才是。</p> <p>系统圆角需要裁剪 layer 中间的 contents ,这其中裁剪工作和离屏渲染对性能的影响哪个占的比重大?我对此有点疑问。虽然系统圆角下裁剪工作和离屏渲染无法拆分,但可以单独测试出裁剪工作对性能的影响。我使用上面提到的某篇优化圆角的文章提供的 Demo 在快速滚动下得到的帧率如下,在此基础上验证测试:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3f8e47123913d865f1f5b2959770a89b.png"></p> <p style="text-align:center">基础帧率</p> <p>图中括号内的数量代表滚动时同屏下圆角效果的个数。同时测试了圆角半径对性能的影响,两者没有关系, cornerRadius 分别为0.1和10的时候无明显差别。使用「Color Offscreen-Renderd Yellow」来检测时,只有圆角部分才会有黄色特征,因此在 cornerRadius = 0.1 的时候基本观测不到,如果你对 cornerRadius 和 masksToBounds 合体才能触发离屏渲染有疑问,对比帧率就知道了。</p> <p>这个 Demo 里的优化方案是重绘圆角,作者给出了他在 iPhone 6 上的测试结果,非常好。奇怪的是 Demo 里没有将绘制圆角的工作放到后台,文章里没有对此进行解释,不过这个 Demo 在我服役多年的 iPad mini 1代(iOS 9.3.1)上的运行结果是无法让人满意的,显然应该放在后台重绘再切换到主线程设置内容。做个对比测试,前台圆角:主线程绘制圆角(Demo 的优化方法),后台圆角:将原 Demo 的绘制操作放到后台线程然后切换到主线程,同屏圆角数量为24个,对比结果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3f8e47123913d865f1f5b2959770a89b.png"></p> <p style="text-align:center">圆角对比</p> <p>前台圆角的性能稍好于系统圆角,后台圆角的表现和无圆角持平。经过测试, masksToBounds=true 和 cornerRadius>0 在单独作用的时候对性能基本没有影响(针对无圆角,前台圆角和后台圆角),且单独作用下无法观察到离屏渲染时的黄色特征,也就是说只有系统圆角才触发了离屏渲染。</p> <p>对比上面的测试结果,眼看就要得出「在系统圆角中(阻塞主线程的)裁剪工作是影响性能的主要因素,黑锅不该离屏渲染来背。」的结论来了。视图性能出现问题时,要分清瓶颈是在 CPU 还是 GPU 上,使用 GPU Driver Instruments 来检测。以下测试中同屏圆角数量在24个左右:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6a517fd646bdb12ff5cdce8867bce3f4.png"></p> <p style="text-align:center">系统圆角</p> <p>系统圆角: 帧率很低,CPU 利用率较低,GPU 利用率很高</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/47954b7282320ab1fb575e02a4751073.png"></p> <p style="text-align:center">前台圆角</p> <p>前台圆角:帧率比上面稍好,不稳定,CPU 利用率起伏很大,高峰接近100%,低谷在20%以下,GPU 利用率很低</p> <p><img src="https://simg.open-open.com/show/3987d177e8da39d576406d482371aaed.png"></p> <p style="text-align:center">后台圆角</p> <p>后台圆角:帧率非常好,CPU 利用率起伏非常大,高峰超过120%,低谷在10%以下,GPU 利用率很低</p> <p>惨遭打脸!有点意外的是前台圆角的 CPU 使用率和后台圆角一样起伏都很大。重绘圆角时,绘制工作是由 CPU 完成的,这可能成为性能的瓶颈,在系统圆角下 GPU 是瓶颈,由于无法将离屏渲染和我所谓的裁剪工作分开,之前试图用自行绘制圆角妄图证明系统圆角里裁剪圆角的工作是影响性能主因的对比测试是没有意义的。</p> <p><strong><a href="/misc/goto?guid=4959663558042605758" rel="nofollow,noindex">Mastering UIKit Performance</a></strong> 里介绍离屏渲染时也举了圆角的例子,他给出的代码并没有在后台绘制圆角,另一方面他表示绘制圆角的代码只会执行一次(在实际使用时的确应该这样设计,只绘制一次,后续直接使用重绘的结果),但从贴出来的代码来看绘制代码无法只执行一次(毕竟是 Demo,没有优化这一点,实际上就变成了和系统圆角一样,滚动的每一帧都在重绘),这样一来就变成了在主线程进行手工绘制圆角,优化效率不高,而且从最后贴出的帧率截图来看并没有达到结论所说的那样高帧率以及稳定性。由于这篇文章并没有开发源代码,无法探明其中的差异。他的测试硬件是 iPhone 4(iOS 7.1.1),而我的 iPad mini 1代与 iPhone 4相差两年,上面的 Demo 里的测试硬件是 iPhone 6,又相差2年,考虑到硬件性能的差异,重绘圆角应该放到后台才是最优解。</p> <h2><strong>OffscreenRenderDemo</strong></h2> <p>还有其他的几个效果需要测试,所以还是要写个 Demo 的: <a href="/misc/goto?guid=4959714508661539721" rel="nofollow,noindex">OffscreenRenderDemo</a> ,里面包括本文涉及的所有效果演示以及优化方案。测试的 Demo 还是老一套,TableView 配合图像和文本,长这样,接下来的效果测试都主要集中在左侧的两个 UIImageView 上,尺寸都为(80, 80),cell 高度为100。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a7c18289d8df46b0eaff132f8bc04f92.png"></p> <p style="text-align:center">界面元素</p> <p>测试环境为:</p> <ul> <li>iPad mini 1st generation with iOS 9.3.1</li> <li>Xcode 7.3 with Swift 2.2</li> <li>OS X 10.11.4</li> </ul> <p>在 Demo 里实现了圆角的优化,这个话题还没有结束呢,上一节只是证明了界面滚动过程中大量的离屏渲染确实是帧率杀手。再放图就特别占地方了,接下来就用表格来呈现数据,数据是我目测计算出来的,会有误差,而且是单次测试,但是量级是没有问题的。接下来的描述中:左右代表在某个值附近浮动,以下代表都接近某个值,很少有超过的,以上代表绝大部分在某个值以上,但超过幅度不大。</p> <p>OffscreenRenderDemo 的基准性能:</p> <table> <thead> <tr> <th>同屏系统圆角数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>CPU 利用率</th> </tr> </thead> <tbody> <tr> <td>无</td> <td>57以上</td> <td>10%以下</td> <td>20%~40%</td> </tr> <tr> <td>10</td> <td>44左右</td> <td>80%以上</td> <td>10%~50%</td> </tr> <tr> <td>20</td> <td>35左右</td> <td>90%以下</td> <td>10%~40%</td> </tr> </tbody> </table> <p>CPU 的利用率很难用平均数值呈现,从上面也可以看到 CPU 的利用率是周期性的波动,这是这类 Demo 的特点,这导致很难对比两次测试中的 CPU 利用率。上面的表格里标注的波动范围仅能当作 CPU 是否是性能瓶颈的参考,而不能与其他测试进行对比。上面的图里 CPU 的采样间隔是1ms,FPS 和 GPU 的利用率的采样间隔是1s,这些是默认值。如果你希望增大 CPU 采样间隔时间来形成类似的柱状图,基本上没有意义,这里的数据是累计利用率,稍不注意看到的都超过100%。触发离屏渲染的效果的瓶颈主要是 GPU,CPU 的利用率偏低,当然,视图性能跟 CPU 和 GPU 都有关,后面的效果会对 CPU 的利用率做出说明。</p> <p>在我的 Demo 里,后台绘制圆角自不必说和无任何效果下的性能非常接近,在主线程绘制圆角的性能只是略微下降。这与上一个 Demo 的相关情况相差很大,上面的结果显示在同屏幕圆角数量24个的情况下,平均帧率勉强在40左右,修正为20个测试一次,平均帧率依然在40附近徘徊。我的 Demo 在主线程以及后台线程绘制圆角时 CPU 的利用率也不像上一个 Demo 那样变化剧烈。由于代码的差异,这些情况很难说明什么,但再次证明一点,为了高帧率,后台绘制才是最优解。</p> <p>大部分赚星星的方案都采用了重绘圆角,重绘的方式有多种,都是殊途同归。实际中重绘圆角的优化方案需要考虑的是,将图像重新绘制为为圆角图像相当于多了一份拷贝,要不要缓存?A.第一次重绘后将这些圆角图像缓存在磁盘里,第二次加载直接使用缓存的圆角图像;B.直接保存在内存里,在内存比较吃紧时显然不是个好选择;C.不缓存,和系统圆角一样,每次都重绘,浪费电量。</p> <p>说了这么多,重绘方案与其他的优化方案相比,并没有什么优势。来看看其他方案:</p> <ol> <li>如果不需要对外部来源的图片做圆角,由设计师直接画成圆角图片是最方便的;</li> <li>混合图层:在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。 <a href="/misc/goto?guid=4958875629456518550" rel="nofollow,noindex">VVebo</a> 微博客户端就是这样做的,遮挡的部分背景最好与周围背景相同。多一个图层会增加合成的工作量,但这点工作量与离屏渲染相比微不足道,性能上无论各方面都和无效果持平。下面左侧的图像是 VVebo 里用来制造圆形头像的 mask 图像,实际中有这种需求的基本是制造圆形头像,普通的圆角遮罩需要左二这种,左三是通用型。如果叠加的视图都一样,可以只加载一次遮罩图片以减少内存占用。</li> </ol> <p style="text-align:center"><img src="https://simg.open-open.com/show/5d32be2a9501f2d9b68fc0559f6618ac.png"></p> <p style="text-align:center">遮罩</p> <p>除了用软件画出来保存在项目里,直接用代码画出来也是很简单的。即使不熟悉 Core Graphics 的 API,搜索出来的重绘圆角的代码看懂是很容易的,但要绘制出上面的图形还是有点棘手。这种事情多试试就好了:在一个设置 opaque = false 的 CGContext 里,设定填充颜色然后用两条贝塞尔曲线围成一个封闭区域,最后从这个绘制环境导出图像即可。我写了个函数来生成区域圆角遮罩图像: <a href="/misc/goto?guid=4959714508781833160" rel="nofollow,noindex">Draw a transparent image</a> 。</p> <p>如何在文本视图类上实现圆角?文本视图主要是这三类:UILabel, UITextField, UITextView。其中 UITextField 类自带圆角风格的外型,UILabel 和 UITextView 要想显示圆角需要表现出与周围不同的背景色才行。想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的 contents 呈现透明的背景色,文本视图类的 layer 的 contents 默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的 backgroundColor ,再加上 cornerRadius 就可以搞定了。不过 UILabel 上设置 backgroundColor 的行为被更改了,不再是设定 layer 的背景色而是为 contents 设置背景色,UITextView 则没有改变这一点,所以在 UILabel 上实现圆角要这么做:</p> <pre> <code class="language-objectivec">//不要这么做:label.backgroundColor = aColor 以及不要在 IB 里为 label 设置背景色 label.layer.backgroundColor = aColor label.layer.cornerRadius = 5 </code></pre> <h2><strong>Shadow</strong></h2> <p><a href="/misc/goto?guid=4959714508861794696" rel="nofollow,noindex">Shadow Properties</a> 展示了阴影是如何与视图本身结合的:</p> <p><img src="https://simg.open-open.com/show/9769d5980df838230d48187d5393ac52.png"></p> <p style="text-align:center">Layer displaying the shadow properties</p> <p>阴影直接合成在视图的下面,视图结构里并没有多出一个视图。在没有指定阴影路径时,阴影是沿着视图的非透明部分扩展的,而且 CALayer 的三个视觉元素至少有一个存在时才会有阴影。</p> <p>使用阴影必须保证 layer 的 masksToBounds = false ,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常这样实现一个阴影:</p> <pre> <code class="language-objectivec">letimageViewLayer = avatorView.layer imageViewLayer.shadowColor = UIColor.blackColor().CGColor imageViewLayer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示 imageViewLayer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响 imageViewLayer.shadowOffset = CGSize(width: 5, height: 5) //设定路径:与视图的边界相同 letpath = UIBezierPath(rect: cell.imageView.bounds) imageViewLayer.shadowPath = path.CGPath//路径默认为 nil </code></pre> <p>在 OffscreenRenderDemo 里,仅开启阴影(没有指定路径,同屏数量10个以上)在滚动时帧率会大幅下降,检测到离屏渲染的黄色特征;指定一个与边界相同的简单路径后离屏渲染特征消失,帧率恢复正常。</p> <p>测试结果:</p> <table> <thead> <tr> <th>条件</th> <th>同屏 SHADOW 数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>离屏渲染特征</th> </tr> </thead> <tbody> <tr> <td>shadowPath = nil</td> <td>10</td> <td>38左右</td> <td>73%左右</td> <td>有</td> </tr> <tr> <td>shadowPath != nil</td> <td>10</td> <td>56以上</td> <td>15%以下</td> <td>无</td> </tr> <tr> <td>shadowPath = nil</td> <td>20</td> <td>22左右</td> <td>80%左右</td> <td>有</td> </tr> <tr> <td>shadowPath != nil</td> <td>20</td> <td>56以上</td> <td>20%以下</td> <td>无</td> </tr> </tbody> </table> <p>为阴影指定路径前后 CPU 的利用率无明显变化,大部分时间都在50%以下,无法判断设定路径是否增加了 CPU 的负担。这里要吐槽下 CALayer 的设计, shadowPath 默认值为 nil,然而效果是与当视图边界路径一致,如果 CALayer 默认添加与边界相同的路径完全可以避免这个问题。</p> <p>除了指定路径,实现良好性能阴影的方法还有:用圆角优化里混合图层的方法模拟阴影的效果:放一个同样效果的视图在要添加阴影程度的视图的下方;使用 Core Graphics 绘制阴影,不过除非万不得已没人想碰 Core Graphics API。从实现成本来讲,都不如指定路径方便。这两种方法实现简单形状的阴影比较方便,比如图中左侧和中间的效果,面对右侧的阴影效果就不好弄了,用指定路径的方法实现也比较麻烦,还好,有更简单方便的优化方法,看压轴章节。</p> <h2><strong>Mask</strong></h2> <p>Mask 效果与混合图层的效果非常相似,只是使用同一个遮罩图像时,mask 与混合图层的效果是相反的,在 Demo 里使用反向内容的遮罩来实现圆角。实现 mask 效果使用 CALayer 的 layer 属性,在 iOS 8 以上可以使用 UIView 的 maskView 属性。代码:</p> <pre> <code class="language-objectivec">if #available(iOS 8.0, *) { avatorView.maskView = UIImageView(image: maskImage) } else { letmaskLayer = CALayer() maskLayer.frame = avatorView.bounds maskLayer.contents = maskImage?.CGImage avatorView.layer.mask = maskLayer } </code></pre> <p>如果所有 maskImage 相同的话,使用一个 maskImage 就够了,不然每次生成一个新的 UIImage 也会是一个性能隐患点。注意:可以使用同一个 maskImage,但不能使用同一个 maskView,不然同时只会有一个 mask 效果。</p> <p>测试结果:</p> <table> <thead> <tr> <th>同屏 MASK 数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>离屏渲染特征</th> </tr> </thead> <tbody> <tr> <td>10</td> <td>55左右</td> <td>60%左右</td> <td>有</td> </tr> <tr> <td>20</td> <td>37左右</td> <td>75%左右</td> <td>有</td> </tr> </tbody> </table> <p>maskImage 的透明面积是否影响性能?粗略测试,并无影响,至少在 Demo 里 Size(80, 80) 这种级别的尺寸下没有什么明显影响。</p> <p>两组测试的 CPU 利用率大部分时间都在50%以下,无明显差别。看第1组数据,很有意思,在同屏 mask 数量为10的情况下,性能几乎无影响,尽管此时 GPU 的利用率有点偏高,但是还能搞得定,保证了滚动的流畅;mask 数量增长到20后,GPU 使用率涨幅明显,在 mask 数量为10的情况 GPU 的利用率已经偏高,数量增加10后,GPU 撑不住了,滚动帧率下降得很厉害。与前面两种效果相比,mask 引发的离屏渲染对性能的影响弱一些。</p> <p>Mask 效果无法取消离屏渲染,使用混合图层的方法来模拟 mask 效果,性能各方面都是和无效果持平。</p> <p>使用 mask 来实现圆角时也可以不用图片,而使用 CAShapeLayer 来指定混合的路径。</p> <pre> <code class="language-objectivec">letroundedRectPath = UIBezierPath(roundedRect: avatorView.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 10, height: 10)) letshapeLayer = CAShapeLayer() shapeLayer.path = roundedRectPath.CGPath avatorView.layer.mask = shapeLayer </code></pre> <p>同样的 mask 效果使用 CAShapeLayer 时相比直接使用 maskImage 在帧率上稍低,CPU 利用率无明显变化,但是 GPU 利用率也低一些。</p> <p><a href="/misc/goto?guid=4959714508116982977" rel="nofollow,noindex">WWDC 2014: Advanced Graphics and Animations for iOS Apps</a> 里详细讲解了 mask 效果的渲染过程,老实说看上去和合成两个视图差不了多少,不过没有更多的细节不知道两者性能的差别在哪里。而且按照这个 session 的说法,系统圆角使用 mask 的方式实现的,不过显然没有优化好。另外这个 session 里 GPU Driver 还叫 Open GL ES Driver。</p> <h2><strong>GroupOpacity</strong></h2> <p>首先来看看 GroupOpacity 是什么效果:</p> <p><img src="https://simg.open-open.com/show/9ae3366edaf9bf1a6d64020b60d10c8f.png"></p> <p>GroupOpacity Sample.png</p> <p>GroupOpacity 是指 CALayer 的 allowsGroupOpacity 属性,UIView 的 alpha 属性等同于 CALayer opacity 属性。开启 GroupOpacity 后,子 layer 在视觉上的透明度的上限是其父 layer 的 opacity 。</p> <p>这个属性的文档说明:</p> <p>The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.</p> <p>从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。</p> <p>GroupOpacity 开启离屏渲染的条件是: layer.opacity != 1.0 并且有子 layer 或者背景图。</p> <p>这个触发条件并不需要 subLayer.opacity != 1.0 ,非常容易满足。然而在 TableView 这样的视图里设置 cell 或 cell.contentView 的 alpha 属性小于1并不能检测离屏渲染的黄色特征,性能上也没有明显差别。经过摸索发现:只有设置 tableView 的 alpha 小于1时才会触发离屏渲染,对性能无明显影响;设置 cell 的 alpha 属性并不会对整体的透明度产生影响,只有设置 cell.contentView 才有效。</p> <p>在一般的 UIViewController 的视图下可以很容易地观察到 GroupOpacity 触发的离屏渲染,这里只能猜测 TableView 更改了这些行为。</p> <h2><strong>EdgeAntialiasing</strong></h2> <p>经过测试,开启 edge antialiasing(旋转视图并且设置 layer.allowsEdgeAntialiasing = true ) 在 iOS 8 和 iOS 9 上并不会触发离屏渲染,对性能也没有什么影响,也许到现在这个功能已经被优化了。</p> <h2><strong>终极优化方案</strong></h2> <p>除了 GroupOpacity 和 EdgeAntialiasing,其他效果触发的离屏渲染都会对性能产生严重影响,离屏渲染真的是一无是处吗?不,离屏渲染本来是个优化设计。如何物尽其用?答案是:Rasterization。在 OffscreenRenderDemo 里,只需要这么做:</p> <pre> <code class="language-objectivec">cell.layer.shouldRasterize = true cell.layer.rasterizationScale = cell.layer.contentsScale </code></pre> <p>shouldRasterize = false 时,离屏渲染的黄色特征仅限于上述自动触发离屏渲染的效果的部分, shouldRasterize = true 后该部分和开启了该属性的 layer 整体(在这里就是 cell 整体)都有黄色特征,所以开启 Rasterization 是手动启动了离屏渲染。</p> <p>从前面来看,离屏渲染会给 GPU 带来沉重的负担,强制启动岂不是更糟?开启 Rasterization 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过 100ms 没有使用会从缓存里移除,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,原本拖后腿的离屏渲染就成为了助力;如果视图内容是动态变化的,使用这个方案有可能让性能变得更糟。</p> <p>Core Animation Instruments 有个「Color Hits Green and Misses Red」的选项,开启 Rasterization 后开启这个选项,屏幕上绿色的部分表示有渲染缓存可用,红色的部分表示无渲染缓存可用。在 OffscreenRenderDemo 里,针对以上任何一个效果开启 Rasterization 后,滚动时还在屏幕范围内的视图会复用缓存的渲染结果,可以看到这部分被标记为绿色,即将出现在屏幕上,处于滚动边缘范围的视图被标记为红色。</p> <p>默认情况下, shouldRasterize 属性为 false 。开启后与原来的测试对比:</p> <table> <thead> <tr> <th>条件</th> <th>同屏系统圆角数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>离屏渲染特征</th> </tr> </thead> <tbody> <tr> <td>shouldRasterize = false</td> <td>10</td> <td>44左右</td> <td>80%以上</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>10</td> <td>55以上</td> <td>20%以下</td> <td>有</td> </tr> <tr> <td>shouldRasterize = false</td> <td>20</td> <td>35左右</td> <td>90%以下</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>20</td> <td>55左右</td> <td>20%左右</td> <td>有</td> </tr> </tbody> </table> <table> <thead> <tr> <th>条件</th> <th>同屏 SHADOW 数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>离屏渲染特征</th> </tr> </thead> <tbody> <tr> <td>shouldRasterize = false</td> <td>10</td> <td>38左右</td> <td>73%左右</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>10</td> <td>55以上</td> <td>30%以下</td> <td>有</td> </tr> <tr> <td>shadowPath != nil</td> <td>10</td> <td>56以上</td> <td>15%以下</td> <td>无</td> </tr> <tr> <td>shouldRasterize = false</td> <td>20</td> <td>22左右</td> <td>80%左右</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>20</td> <td>55左右</td> <td>40%以下</td> <td>有</td> </tr> <tr> <td>shadowPath != nil</td> <td>20</td> <td>56以上</td> <td>20%以下</td> <td>无</td> </tr> </tbody> </table> <p>以上 Rasterization 与 shadowPath 至少保留一个默认设置。与指定路径相比,Rasterization 的 GPU 利用率要高一些。</p> <table> <thead> <tr> <th>条件</th> <th>同屏 MASK 数量</th> <th>平均 FPS</th> <th>GPU 平均利用率</th> <th>离屏渲染特征</th> </tr> </thead> <tbody> <tr> <td>shouldRasterize = false</td> <td>10</td> <td>55左右</td> <td>60%左右</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>10</td> <td>55以上</td> <td>20%左右</td> <td>有</td> </tr> <tr> <td>shouldRasterize = false</td> <td>20</td> <td>37左右</td> <td>75%左右</td> <td>有</td> </tr> <tr> <td>shouldRasterize = true</td> <td>20</td> <td>55左右</td> <td>30%以下</td> <td>有</td> </tr> </tbody> </table> <p>从上面的数据来看,Rasterization 的优化效果是非常给力的。对于 GPU 而言,利用率在60%以下时,界面能够维持较高的帧率。</p> <p>前面提到如果视图内容是动态变化的,使用 Rasterization 有可能让性能变得更糟。什么情况下会遇到动态内容的视图呢,能想到的只有后台下载图片完毕后切换到主线程设置这种了。来模拟下,在 tableView:cellForRowAtIndexPath: 里调用以下方法:</p> <pre> <code class="language-objectivec">funcdynamicallyUpdateCell(cell: UITableViewCell){ letnumber = Int(UInt32(arc4random()) % UInt32(10)) letlabelL = cell.viewWithTag(30) as! UILabel labelL.text = "OffscreenRender" + String(number) letavatorViewL = cell.viewWithTag(10) as! UIImageView avatorViewL.layer.cornerRadius = CGFloat(number) avatorViewL.clipsToBounds = true letdelay = NSTimeInterval(number) * 0.1 performSelector(#selector(TableViewController.dynamicallyUpdateCell(_:)), withObject: cell, afterDelay: delay) } </code></pre> <p>这段代码随机时间内更新 UILabel 的内容和头像圆角半径,这里只设置了一半的视图。下面是开启 Rasterization 后同时设置两个头像和两个 label 的性能,这里 GPU 的高峰在50%左右,CPU 的高峰接近100%,FPS 的高峰在55左右,低谷为20左右。</p> <p><img src="https://simg.open-open.com/show/cee72d734aff5edcd0d06ac0d8da5f86.png"></p> <p style="text-align: center;">动态视图</p> <p>应用启动后前8秒无操作;8~20秒滚动视图;20~32秒无操作;32~42秒滚动视图;42~56秒无操作;00:56~01:12滚动视图;01:12~结束无操作。这里除了20~32秒 FPS 有点反常地高,其他都比较有规律:在无操作时 FPS 很低,在20左右,CPU 满载,GPU 利用率也在高峰,大约50%;视图滚动时 FPS 很高,在50以上,CPU 和 GPU 的利用率都有下降。</p> <p>还需要了解的信息是:主线程繁忙的时候 performSelector:withObject:afterDelay: 会延后执行,所以在发生触摸或是视图还在滚动时这个方法不会运行;用「Color Hits Green and Misses Red」观察离屏渲染对缓存的使用发现:GPU 能够使用视图部分内容的缓存,而不是每次更新都要重新渲染整个视图,提升了渲染的效率。所以在视图没有滚动时并且 dynamicallyUpdateCell: 还在不停调用自身时可以看到画面是红绿斑驳的。</p> <p>根据以上两段信息来分析性能走势:应用启动后前8秒 CPU 的走势还是挺随机的,间歇性地达到较高的占用率,这一阶段 CPU 是性能瓶颈,FPS 很低。视图滚动时,由于 performSelector 不会执行,和普通的 tableView:cellForRowAtIndexPath: 方法调用并无二致,CPU 的利用率不高,在 Rasterization 的作用下,GPU 的利用率也不高,FPS 大幅提升;而视图停止滚动后,performSelector 开始执行,似乎累计到一起的工作让刻意设置的随机性失去了作用,CPU 时刻满载,GPU 的利用率也随之提升,而得益于 Rasterization,并没有到很高的地步,但由于 CPU 的满载,FPS 降到很低。</p> <p>从结果来看,开启 Rasterization 后 GPU 的利用率始终不高,如果 CPU 的利用率控制得当的话 FPS 不会难看,比预计的性能要好多了。</p> <h2>总结</h2> <ol> <li>RoundedCorner 在仅指定 cornerRadius 时不会触发离屏渲染,仅适用于特殊情况: contents 为 nil 或者 contents 不会遮挡背景色圆角;</li> <li>Shawdow 可以通过指定路径来取消离屏渲染;</li> <li>Mask 无法取消离屏渲染;</li> </ol> <p>以上效果在同等数量的规模下,对性能的影响等级:Shadow > RoundedCorner > Mask > GroupOpacity(迷之效果)。</p> <p>任何时候优先考虑避免触发离屏渲染,无法避免时优化方案有两种:</p> <ol> <li>Rasterization:适用于静态内容的视图,也就是内部结构和内容不发生变化的视图,对上面的所有效果而言,在实现成本以及性能上最均衡的。即使是动态变化的视图,开启 Rasterization 后能够有效降低 GPU 的负荷,不过在动态视图里是否启用还是看 Instruments 的数据。</li> <li>规避离屏渲染,用其他手法来模拟效果,混合图层是个性能最好、耗能最少的通用优化方案,尤其对于 rounded corer 和 mask。</li> </ol> <p> </p> <p> </p> <p>来自:http://ios.jobbole.com/88618/</p> <p> </p>
本文由用户 sbicrgw 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!