在使用UIKit的过程当中,性能优化是永恒的话题。不少人都看过度析优化滑动性能的文章,但其中很多文章只介绍了优化方法却对背后的原理避而不谈,或者是晦涩难懂并且读者缺少实践体验的机会。不妨思考一下下面的问题本身是否有一个清晰的认识:ios
shouldRasterize
和离屏渲染的关系是什么,什么时候应该使用?本文会结合Instrument分析影响性能的因素,提出优化方案并解释背后的原理,项目初始demo的下载地址在个人Github,强烈建议每一位读者下载下来随着我一步一步调试、优化。若是以为对本身有帮助,能够给一个Star表示支持。后面的图片较多,流量党慎入。git
打开项目后,只要CustomTableCell.swift文件便可,它实现了自定义的UITableViewCell
以及内部的UI布局,由于重点在于性能优化,代码实现的就比较随意。github
首先按下Command + I
打开Instrument,本文主要用到的是Core Animation工具:swift
注意这个调试必须使用真机,点击左上角的红色圆圈就会开始录制。新手可能不太熟悉,这里简单介绍一下调试界面:缓存
咱们须要了解两个两个区域:性能优化
有过游戏经验的人也许对fps这个概念比较熟悉。咱们知道任何屏幕老是有一个刷新率,好比iphone推荐的刷新率是60Hz,也就是说GPU每秒钟刷新屏幕60次,所以两次刷新之间的间隔为16.67ms。这段时间内屏幕内容保持不变,称为一帧(frame),fps表示frames per second,也就是每秒钟显示多少帧画面。对于静止不变的内容,咱们不须要考虑它的刷新率,但在执行动画或滑动时,fps的值直接反映出滑动的流畅程度。bash
首先咱们要明白像素的概念,屏幕上每个点都是一个像素,像素有R、G、B三种颜色构成(有时候还带有alpha值)。若是某一块区域上覆盖了多个layer,最后的显示效果受到这些layer的共同影响。举个例子,上层是蓝色(RGB=0,0,1),透明度为50%,下层是红色(RGB=1,0,0)。那么最终的显示效果是紫色(RGB=0.5,0,0.5)。这种颜色的混合(blending)须要消耗必定的GPU资源,由于实际上可能不止只有两层。若是只想显示最上层的蓝色,能够把它的透明度设置为100%,这样GPU会忽略下面全部的layer,从而节约了不少没必要要的运算。网络
第一个调试选项"Color Blended Layers"正是用于检测哪里发生了图层混合,并用红色标记出来。所以咱们须要尽量减小看到的红色区域。一旦发现应该想法设法消除它。开始调试后勾选这个选项,咱们在手机上能够看到以下的场景:app
不少文章里说把控件设置为opaque = true
,其原理就是但愿避免图层混合,然而这种调优通常状况下用处不大。由于UIView
的opaque
属性默认值就是true
,也就是说只要不是人为设置成透明,都不会出现图层混合。好比demo中就没有任何透明的控件。iphone
对于UIImageView
来讲,不只它自身须要是不透明的,它的图片也不能含有alpha通道,这就是为何图中第三个图片是绿色,而前两个图片是红色的缘由。因为本人对PS和图像几乎一窍不通,恕我不能演示如何消除这些图片的红色。我从网上找了一个美女的头像来讲明,图像自身的性质可能会对结果有影响,所以若是你肯定本身的代码没有问题,并且出现了图层混合,请联系美工或后台解决。
我的认为比opaque
属性更重要的是backgroundColor
属性,若是不设置这个属性,控件依然被认为是透明的,因此咱们作的第一个优化是在CustomTableCell
类的init
方法中添加一行代码:
label.backgroundColor = UIColor.whiteColor()
复制代码
虽然在白色背景下,这行代码没法肉眼看到效果,但从新调试后咱们能够发现label的红色消失了。也正是由于对背景颜色的不重视,它成了影响滑动性能的第一个杀手。
PS:若是label文字有中文,依然会出现图层混合,这是由于此时label多了一个sublayer
,若是有好的解决办法欢迎告诉我。
光栅化是将一个layer预先渲染成位图(bitmap),而后加入缓存中。若是对于阴影效果这样比较消耗资源的静态内容进行缓存,能够获得必定幅度的性能提高。demo中的这一行代码表示将label的layer光栅化:
label.layer.shouldRasterize = true
复制代码
Instrument中,第二个调试选项是“Color Hits Green and Misses Red”,它表示若是命中缓存则显示为绿色,不然显示为红色,显然绿色越多越好,红色越少越好。勾选这个选项后咱们看到以下的场景:
光栅化的核心在于缓存的思想。咱们本身动手把玩一下,能够发现如下几个有意思的现象:
这是由于layer进行光栅化后渲染成位图放在缓存中。当屏幕出现滑动时,咱们直接从缓存中读取而没必要渲染,因此会看到绿色。当新的label出现时,缓存中没有个这个label的位图,因此会变成红色。第三点比较关键,缓存中的对象有效期只有100ms,即若是在0.1s内没有被使用就会自动从缓存中清理出去。这就是为何停留一下子再滑动就会看到红色。
光栅化的缓存机制是一把双刃剑,先写入缓存再读取有可能消耗较多的时间。所以光栅化仅适用于较复杂的、静态的效果。经过Instrument的调试发现,这里使用光栅化常常出现未命中缓存的状况,若是没有特殊须要则能够关闭光栅化,因此咱们作的第二个优化是注释掉下面这行代码:
// label.layer.shouldRasterize = true
复制代码
光栅化会致使离屏渲染,这一点待会儿会讲。
像素在内存中的布局和它在磁盘中的存储方式并不相同。考虑一种简单的状况:每一个像素有R、G、B和alpha四个值,每一个值占用1字节,所以每一个像素占用4字节的内存空间。一张1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600个像素,所以占用了超过8Mb的内存。可是一张一样分辨率的PNG格式或JPEG格式的图片通常状况下不会有这么大。这是由于JPEG将像素数据进行了一种很是复杂且可逆的转化。
当咱们打开JPEG格式的图片时,CPU会进行一系列运算,将JPEG图片解压成像素数据。显然这个工做会消耗很多时间,因此不该该在滑动时进行,咱们应该预先处理好图片。借用WWDC上的一页PPT来讲明:
Commit Transaction和Decode在同一帧内进行,若是这两个操做的耗时超过16.67ms,Draw Calls就会延迟到下一帧,从而致使fps值的下降。下面是Commit Transaction的详细流程:
在第三步的Prepare中,CPU主要处理两件事:
好比应用中有一些从网络下载的图片,而GPU刚好不支持这个格式,这就须要CPU预先进行格式转化。第三个选项“Color Copied Images”就用来检测这种实时的格式转化,若是有则会将图片标记为蓝色。
遗憾的是因为我对图片格式不太了解,也不会使用相关工具,并无能模拟出触发这个选项的场景。咱们要记住的是,若是调试时发现有图片被标记为蓝色,说明图片格式出现了一些问题。
第四个选项的使用场景很少,咱们直接看一下第五个选项“Color Misaligned Images”。它表示若是图片须要缩放则标记为黄色,若是没有像素对齐则标记为紫色。勾选上这个选项并进行调试,能够看到以下场景:
在demo中,每一个UIImageView
的大小都是180x180,而只有第二张图片的像素大小是360x360。所以除了第二张图片,其余的图片都须要被缩放。图片的缩放须要占用时间,所以咱们要尽量保证不管是本地图片仍是从网络或取得图片的大小,都与其frame保持一致。
第三个优化是调整全部图片的像素大小以免没必要要的缩放。
离屏渲染表示渲染发生在屏幕以外,你可能认为这是一句废话。为了真正解释清楚什么是离屏渲染,咱们先来看一下正常的渲染通道(Render-Pass):
首先,OpenGL提交一个命令到Command Buffer,随后GPU开始渲染,渲染结果放到Render Buffer中,这是正常的渲染流程。可是有一些复杂的效果没法直接渲染出结果,它须要分步渲染最后再组合起来,好比添加一个蒙版(mask):
在前两个渲染通道中,GPU分别获得了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把二者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,所以相对于普通渲染更占用资源。
第六个选项“Color Offscreen-Rendered Yellow”会把须要离屏渲染的地方标记为黄色,大部分状况下咱们须要尽量避免黄色的出现。离屏渲染可能会自动触发,也能够手动触发。如下状况可能会致使触发离屏渲染:
- 重写drawRect方法
- 有mask或者是阴影(layer.masksToBounds, layer.shadow*),模糊效果也是一种mask
- layer.shouldRasterize = true
前二者会自动触发离屏渲染,第三种方法是手动开启离屏渲染。
开始调试并勾选“Color Offscreen-Rendered Yellow”,会看到这样的场景:
若是没有进行第二步优化,你会发现label也是黄色。能够看到tabbar和statusBar也是黄色,这是由于它们使用了模糊效果。图片也是黄色,这说明它也进行了离屏渲染,观察源码后发现主要缘由是它使用了阴影,接下来咱们进行第四个优化,在设置阴影效果的四行代码下面添加一行:
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
复制代码
这行代码制定了阴影路径,若是没有手动指定,Core Animation会去自动计算,这就会触发离屏渲染。若是人为指定了阴影路径,就能够免去计算,从而避免产生离屏渲染。
设置cornerRadius
自己并不会致使离屏渲染,但不少时候它还须要配合layer.masksToBounds = true
使用。根据以前的总结,设置masksToBounds
会致使离屏渲染。解决方案是尽量在滑动时避免设置圆角,若是必须设置圆角,可使用光栅化技术将圆角缓存起来:
// 设置圆角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
复制代码
还记得以前将离屏渲染和渲染路径时的示意图么,离屏渲染的最后一步是把此前的多个路径组合起来。若是这个组合过程能由CPU完成,就会大量减小GPU的工做。这种技术在绘制地图中可能用到。
第七个选项“Color Compositing Fast-Path Blue”用于标记由硬件绘制的路径,蓝色越多越好。
刷新视图时,咱们应该把须要重绘的区域尽量缩小。对于未发生变化的内容则不该该重绘,第八个选项“Flash updated Regions”用于标记发生重绘的区域。一个典型的例子是系统的时钟应用,绝大多数时候只有显示秒针的区域须要重绘:
若是你一步一步作到了这里,我想必定会有很多收益。不过,学而不思则罔,思而不学则殆。动手实践后仍是应该总结提炼,优化滑动性能主要涉及三个方面:
opaque
属性设置为true
,确保backgroundColor
和父视图颜色一致且不透明alpha
值UIImage
没有alpha通道frame
一致,不要在滑动时缩放图片drawRect
方法,设置圆角、阴影、模糊效果,光栅化都会致使离屏渲染本文的demo能够在个人Github上下载,而后一步一步本身体验优化过程。但demo毕竟是刻意搭建的一个环境,我会在我本身的仿写的简书app上不断进行实战优化,欢迎共同窗习交流。
还有一些高质量的问答: