1.关于CALayer 的 shouldRasterize(光栅化)
开启shouldRasterize后,CALayer会被光栅化为bitmap(位图文件),layer的阴影等效果也会被保存到bitmap中;
当我们开启光栅化后,需要注意三点问题。
-
如果我们更新已光栅化的layer,会造成大量的offscreen渲染(离屏渲染)。
因此CALayer的光栅化选项的开启与否需要我们仔细衡量使用场景。只能用在图像内容不变的前提下的:
① 用于避免静态内容的复杂特效的重绘,例如前面讲到的UIBlurEffect(毛玻璃)
② 用于避免多个View嵌套的复杂View的重绘。
而对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。
例如我们日程经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的offscreen渲染,降低图形性能。
当然,合理利用的话,是能够得到不少性能的提高的,因为使用shouldRasterize后layer会缓存为Bitmap位图,对一些添加了shawdow等效果的耗费资源较多的静态内容进行缓存,能够得到性能的提升。
-
不要过度使用,系统限制了缓存的大小为2.5X Screen Size.
如果过度使用,超出缓存之后,同样会造成大量的offscreen渲染。
-
被光栅化的图片如果超过100ms没有被使用,则会被移除
因此我们应该只对连续不断使用的图片进行缓存。对于不常使用的图片缓存是没有意义,且耗费资源的。
2. 关于offscreen rendering
注意到上面提到的offscreen rendering。我们需要注意shouldRasterize的地方就是会造成offscreen rendering的地方,那么为什么需要避免呢?
WWDC 2011 Understanding UIKit Rendering指出一般导致图形性能的问题大部分都出在了offscreen rendering,因此如果我们发现列表滚动不流畅,动画卡顿等问题,就可以想想和找出我们哪部分代码导致了大量的offscreen 渲染。
什么是offscreen-render?offscreen-render涉及的内容比较多,有offscreen-render那就有onscreen render,onscreen render指的是GPU在当前用于显示的屏幕缓冲区进行渲染,相反offscreen-render就是不在当前的屏幕缓存区,而在另外的缓冲区进行渲染,offscreen-render有两种形式:
首先,什么是offscreen rendering?
offscreen rendring指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。
那么为什么offscreen渲染会耗费大量资源呢?
原因是显卡需要另外alloc一块内存来进行渲染,渲染完毕后在绘制到当前屏幕,而且对于显卡来说,onscreen到offscreen的上下文环境切换是非常昂贵的(涉及到OpenGL的pipelines和barrier等),
备注:
这里提到的offscreen rendering主要讲的是通过GPU执行的offscreen,事实上还有的offscreen rendering是通过CPU来执行的(例如使用Core Graphics, drawRect)。其它类似cornerRadios, masks, shadows等触发的offscreen是基于GPU的。
许多人有误区,认为offscreen rendering就是software rendering,只是纯粹地靠CPU运算。实际上并不是的,offscreen rendering是个比较复杂,涉及许多方面的内容。
我们在开发应用,提高性能通常要注意的是避免offscreen rendering。不需要纠结和拘泥于它的定义。
offscreen-render对性能到底有什么影响?通常大家说的离屏渲染指的是GPU这块(当然CPU这块也会有影响,也需要消耗一定的资源),比如修改了layer的阴影或者圆角,GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响,所以可以的话尽量减少offscreen-render的图层,查看哪些图层需要离屏渲染可以用Instruments的Core Animation工具进行检测,Color Offscreen-Rendered Yellow选项会将对应的图层标记为黄色。
有兴趣可以继续阅读Andy Matuschak, 前UIKit team成员关于offscreen rendering的评论。
总之,我们通常需要避免大量的offscreen rendering.
会造成 offscreen rendering的原因有:
-
Any layer with a mask (layer.mask)
-
Any layer with layer.masksToBounds being true
-
Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
-
Any layer with a drop shadow (layer.shadow*).
-
Any layer with layer.shouldRasterize being true
-
Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing
因此,对于一些需要优化图像性能的场景,我们可以检查我们是否触发了offscreen rendering。 并用更高效的实现手段来替换。
例如:
① 阴影绘制:使用ShadowPath来替代shadowOffset等属性的设置。
一个如图的简单tableView:
两种不同方式来绘制阴影:
不使用shadowPath
1
2
3
4
5
|
CALayer *imageViewLayer = cell.imageView.layer; imageViewLayer.shadowColor = [UIColor blackColor].CGColor; imageViewLayer.shadowOpacity = 1.0; imageViewLayer.shadowRadius = 2.0; imageViewLayer.shadowOffset = CGSizeMake(1.0, 1.0); |
使用shadowPath
cell.sign.layer.shadowOffset = CGSizeMake(0, 2);
cell.sign.layer.shadowOpacity = 0.5;
cell.sign.layer.shadowColor = [UIColor blackColor].CGColor;
但是你可以发现这会导致离屏渲染,一个简单的不需要离屏渲染的方法就是制定阴影的路径,也就是设置layer的shadowPath属性,通过instruments发现阴影的地方没有黄色了,帧率也提高到了40FPS:
cell.sign.layer.shadowPath = [UIBezierPath bezierPathWithRect:cell.sign.bounds].CGPath;
1
|
imageViewLayer.shadowPath = CGPathCreateWithRect(imageRect, NULL); |
我们可以在下图看到两种方式巨大的性能差别。
个人推测的shadowPath高效的原因是使用shadowPath避免了offscreen渲染,因为仅需要直接绘制路径即可,不需要提前读取图像去渲染。
②裁剪图片为圆:
如图为例
使用CornerRadius:
1
2
3
|
CALayer *imageViewLayer = cell.imageView.layer; imageViewLayer.cornerRadius = imageHeight / 2.0; imageViewLayer.masksToBounds = YES; |
利用一张中间为透明圆形的图片来进行遮盖,虽然会引起blending,但性能仍然高于offerScreen。
根据苹果测试,第二种方式比第一种方式更高效:
对于圆角这种类似导致的性能问题,最简单的就是在列表中不要使用圆角,假如要使用圆角的话,一种最快提升性能的方式就是设置layer的shouldRasterize为YES:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
虽然被Rasterize的图层也会引起离屏渲染,如下图所示,整个cell都被标示为黄色:
![](http://upload-images.jianshu.io/upload_images/458529-4bcc28216254d415.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/150)
layer设置shouldRasterize=YES之后,会把被光栅化的图层保存成位图并缓存起来,其中圆角或者阴影之类的效果也是直接保存到位图当中,当需要渲染到屏幕上的时候只需要到缓存中去取对应的位图进行显示就行了,加快了整个渲染过程。可以通过勾选instruments core animation中的Color Hits Green and Misses Red选项来查看图层是否被缓存了,如果图层显示为绿色则表示已经被缓存起来了,也就是这个缓冲区的内容被复用了,不用在去重新创建缓冲区,反之则是用红色标示。如下图可以看到设置shouldRasterize之后,cell都被标示为绿色了,如果滑动过程中发现都是红色的证明就有问题了:
可以发现现在滚动的性能大大提高了,光栅化对于那些有很多子view嵌套在一起、view的层级复杂或者有很复杂特效效果的图层有很明显的提升,因为这些内容都被缓存到位图当中了。但是使用光栅化需要注意一些内容:
- 适用于内容基本不变的图层
假如图层的内容经常变化,比如cell里面有涉及到动画之类的,那么缓存的内容就无效了,GPU需要重新创建缓存区,导致离屏渲染,这又涉及到OpenGL的上下文环境切换,反而降低性能。 - 不要过度使用
缓存区的大小被设置为屏幕大小的2.5倍,假如过分使用同样会导致大量的离屏渲染。 - 如果缓存的内容超过100ms没有被使用则会被回收。
tips
- 对于圆角可以使用一张中间圆形透明的图覆盖在上面,虽然这会引入blending操作,但是大部分情况下性能会比离屏渲染好。
- 让你的view层次结构平坦一些,因为OpenGL在渲染layer的时候,在碰到有子层级layer的时候可能需要停下来把两者合成到一个buffer里再接着渲染。(When the OpenGL renderer goes to draw each layer, it may have to stop for some subhierarchies and composite them into a single buffer).
- 延迟加载图片
有时候在边滚动边设置图片的时候可能会有一定的影响,因此可以在滚动的时候imageview不执行setimage的操作,滚动停止的时候才加载图片,由于滚动的时候NSRunloop是处于UITrackingRunLoopMode (所以 nastime 失效 )模式下,可以采用如下的方式,将设置图片放到NSDefaultRunLoopMode模式下才进行:UIImage *downloadedImage = ...; [self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
- 图片加载的极限优化方式:FastImageCache
③关于blending
前面提到了用透明圆形的图片来进行遮盖,会引起blending。blending也会耗费性能。
:) 笑。如果阅读这篇文章的读者看到这里,是不是觉得已经无眼看下去了。哈哈,我自己学习总结到这里也是感受到了长路慢慢,但是我们仍然还是要不断上下求索的。 :)
好了 接下来让我们来认识一下Blending.
-
什么是Blending?
在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。
-
会导致blending的原因:
layer(UIView)的Alpha < 1
UIImgaeView的image含有Alpha channel(即使UIImageView的alpha是1,但只要image含透明通道,则仍会导致Blending)
-
为什么Blending会导致性能的损失?
原因是很直观的,如果一个图层是不透明的,则系统直接显示该图层的颜色即可。而如果图层是透明的,则会引入更多的计算,因为需要把下面的图层也包括进来,进行混合后颜色的计算。
在了解完Blending之后,我们就知道为什么很多优化准则都需要我们尽量使用不透明图层了。接下来就是在开发中留意和进行优化了。
Blending
假如最上层的view是不透明的,那直接使用这个view的对应颜色之就可以,但如果view是透明的,在计算像素的颜色值时就需要计算它下面图层,透明的视图越多,计算量就越大,因此也会对图形的性能产生一定的影响,所以可以的话也尽量减少透明图层的数目。
(2)测试工具
在出现图像性能问题,滑动,动画不够流畅之后,我们首先要做的就是定位出问题的所在。而这个过程并不是只靠经验和穷举法探索,我们应该用有脉络,有顺序的科学的手段进行探索。
首先,我们要有一个定位问题的模式。我们可以按照这样的顺序来逐步定位,发现问题。
-
定位帧率,为了给用户流畅的感受,我们需要保持帧率在60帧左右。当遇到问题后,我们首先检查一下帧率是否保持在60帧。
-
定位瓶颈,究竟是CPU还是GPU。我们希望占用率越少越好,一是为了流畅性,二也节省了电力。
-
检查有没有做无必要的CPU渲染,例如有些地方我们重写了drawRect,而其实是我们不需要也不应该的。我们希望GPU负责更多的工作。
-
检查有没有过多的offscreen渲染,这会耗费GPU的资源,像前面已经分析的到的。offscreen 渲染会导致GPU需要不断地onScreen和offscreen进行上下文切换。我们希望有更少的offscreen渲染。
-
检查我们有无过多的Blending,GPU渲染一个不透明的图层更省资源。
-
检查图片的格式是否为常用格式,大小是否正常。如果一个图片格式不被GPU所支持,则只能通过CPU来渲染。一般我们在iOS开发中都应该用PNG格式,之前阅读过的一些资料也有指出苹果特意为PNG格式做了渲染和压缩算法上的优化。
-
检查是否有耗费资源多的View或效果。我们需要合理有节制的使用。像之前提到的UIBlurEffect就是一个例子。
-
最后,我们需要检查在我们View层级中是否有不正确的地方。例如有时我们不断的添加或移除View,有时就会在不经意间导致bug的发生。像我之前就遇到过不断添加View的一个低级错误。我们希望在View层级中只包含了我们想要的东西。
是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?
Instruments里的:
-
Core Animation instrument
-
OpenGL ES Driver instrument
模拟器中的:
-
Color debug options View debugging
还有Xcode的:
-
View debugging
然后我们来根据上面定位问题的模式来选择相应测试工具:
-
定位帧率
-
定位瓶颈
-
检查有无必要的CPU渲染
以上三点我们可以使用CoreAnimation instrument来测试。
CoreAnimation instrument包含了两个模块。第一幅图展示了检测帧率。第二幅图展示了检测CPU调用。我们能够通过它们来进行上述三个问题的检测。注意到第二幅图左下角,那是CPU 的call stack.我们就是在这里检测我们有没有做无必要的drawRect,有没有在主线程做太多事务导致阻塞了UI更新。
关于GPU的瓶颈问题,我们可以通过OpenGL ES Driver instrument来获得更详细的信息。例如GPU的占用率。可以看到下图左下角有显示Device utilization。
4. 检查有无过多offscreen渲染
5. 检查有无过多Blending
6. 检查有无不正确图片格式,图片是否被放缩,像素是否对齐。
7. 检查有无使用复杂的图形效果。
以上这四点我们同样使用CoreAnimation instrument来测试。
我们可以看到上图右下角的Debug options有多个选项。我们通过勾选这些选项来触发Color Debug。下面逐个对这些选项进行分析。
-
Color Blended layers
如图,勾选这个选项后,blended layer 混合的图层就会被显示为红色,而不透明的layer则是绿色。我们希望越少红色区域越好。
-
Color Hits Green and Misses Red
这个选项主要是检测我们有无滥用或正确使用layer的shouldRasterize属性.成功被缓存的layer会标注为绿色,没有成功缓存的会标注为红色。
在测试的过程中,第一次加载时,开启光栅化的layer会显示为红色,这是很正常的,因为还没有缓存成功。但是如果在接下来的测试,例如我们来回滚动TableView时,我们仍然发现有许多红色区域,那就需要谨慎对待了。因为像我们前面讨论过的,这会引起offscreen rendering。
检查一下是否有滥用该属性,因为系统规定的缓存大小是屏幕大小的2.5倍,如果使用过度,超出了缓存大小,会引起offscreen rendering。检测layer是否内容不断更新,内容的更新会导致缓存失效和大量的offscreen rendering.
-
Color copied images
这个选项主要检查我们有无使用不正确图片格式,标示那些被Core Animation拷贝的图片。这主要是因为该图片的色彩格式不能被GPU直接处理,需要在CPU这边做转换,假如在主线层做这个操作对性能会有一定的影响。若是GPU不支持的色彩格式的图片则会标记为青色,则只能由CPU来进行处理。我们不希望在滚动视图的时候,CPU实时来进行处理,因为有可能会阻塞主线程。
-
Color misaligned images
这个选项检查了图片是否被放缩,像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。
-
Color offscreen-rendered yellow
这个选项将需要offscreen渲染的的layer标记为黄色。
以上图为例子,NavigationBar和ToolBar被标记为黄色。因为它们需要模糊背后的内容,这需要offscreen渲染。但是这是我们需要的。而图片也是被标记为黄色,那是因为阴影的缘故。我前面已经提到了这一点,如果此时我们用shadowPath来替代的话,就能够避免offscreen渲染带来的巨大开销。
-
Color OpenGL fast path blue
这个选项勾选后,由OpenGL compositor进行绘制的图层会标记为蓝色。这是一个好的结果。
-
Flash updated regions
会标记屏幕上被快速更新的部分为黄色,我们希望只是更新的部分被标记完黄色。
好啦,终于完整介绍完这些调试选项了,我们总结一下。
我们需要重点注意的是
①Color Blended layers
②Color Hits Green and Misses Red
③Color offscreen-rendered yellow这三个选项。
因为这三个部分对性能的影响最大。
8. 检查View层级是否正确。
我们可以在上图清楚地看到View的层级关系。可以检查View的层级是否正确。
小提示(应用运行后,在这里打开):从左往右第七个图
(3)性能优化Image 加载方法
UIImage加载图片方式一般有两种:
A:imagedNamed初始化
B:imageWithContentsOfFile初始化
二者不同之处在于,imageNamed默认加载图片成功后会内存中缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象.如果缓存中没有找到相应的图片对象,则从指定地方加载图片然后缓存对象,并返回这个图片对象.
而imageWithContentsOfFile则仅只加载图片,不缓存.
大量使用imageNamed方式会在不需要缓存的地方额外增加开销CPU的时间来做这件事.当应用程序需要加载一张比较大的图片并且使用一次性,那么其实是没有必要去缓存这个图片的,用imageWithContentsOfFile是最为经济的方式,这样不会因为UIImage元素较多情况下,CPU会被逐个分散在不必要缓存上浪费过多时间.
使用场景需要编程时,应该根据实际应用场景加以区分,UIimage虽小,但使用元素较多问题会有所凸显.