前言在软件开发领域,我们经常听到这样一句话,“过早的优化是万恶之源”,不要过早优化或过度优化。我认为在编码过程中时刻关注对性能的影响是很有必要的,但凡事都有一个限度,不能为了性能而耽误开发进度。在时间紧迫的情况下,我们往往采用“quickanddirty”的方案,快速出结果,后期再迭代优化,这就是所谓的敏捷开发。与之对应的是传统软件开发中的瀑布流开发流程。卡顿的原因在iOS系统中,图像内容在屏幕上显示的过程需要CPU和GPU的共同参与。CPU负责计算显示内容,如视图创建、布局计算、图片解码、文字绘制等,然后CPU将计算出的内容提交给GPU,由GPU进行变换、合成、渲染。之后,GPU会将渲染结果提交到帧缓冲区,等待下一个VSync信号显示在屏幕上。由于垂直同步机制,如果CPU或GPU在一个VSync时间内没有完成内容提交,则该帧将被丢弃,等待下一次机会再次显示,而显示器则保持之前的内容不变。这就是界面冻结的原因。因此,我们需要平衡CPU和GPU的负载,避免一侧过载。为了做到这一点,我们首先需要了解CPU和GPU负责什么。上图展示了各个模块在iOS系统下的位置。下面我们仔细看看CPU和GPU的相应操作。耗CPU任务的布局计算布局计算是iOS中最常消耗CPU资源的地方。如果视图层级复杂,计算所有层的布局信息会消耗一部分时间。因此,我们应该尽量提前计算出布局信息,然后适时调整相应的属性。还要避免不必要的更新,并且仅在发生真正的布局更改时才进行更新。对象的创建对象的创建过程伴随着内存分配、属性设置,甚至文件读取等操作,这些操作都会消耗CPU资源。尝试使用轻型物体而不是重型物体来优化性能。例如,CALayer比UIView轻很多。如果视图元素不需要响应触摸事件,CALayer会更合适。通过Storyboard创建视图对象也会涉及到文件反序列化操作,其资源消耗会比直接通过代码创建对象大很多。在性能敏感的界面中,Storyboard并不是一个好的技术选择。对于列表类型的页面,也可以参考UITableView的复用机制。每次要初始化一个View对象时,先根据标识符从缓存池中取出,能拿到就复用View对象,不能拿到才真正执行初始化过程。滑动屏幕时,滑出屏幕的View对象会根据标识符放入缓冲池,新进入屏幕可见范围的View会根据之前的规则决定是否真正初始化。AutolayoutAutolayout是Apple在iOS6之后推出的一项新的布局技术。在大多数情况下,这种技术可以大大提高开发速度,尤其是需要处理多种语言的时候。比如阿拉伯文的布局是从右到左的,通过Autolayout设置leading和trailing即可。但是,Autolayout通常会导致复杂视图出现严重的性能问题。对于性能敏感的页面,建议使用手动布局,并控制刷新频率,以便在真正需要调整布局时重新布局。文本计算如果一个界面中包含大量文本(如微博、朋友圈等),文本宽高的计算会占用很大一部分资源,无法避免。一个常见的场景是,在UITableView中,heightForRowAtIndexPath方法会被频繁调用。即使不是耗时计算,调用多了也会造成性能损失。这里的优化是为了避免每次都重新计算文本的行高。获取到Model数据后,就可以根据文本内容计算出布局信息,然后将这个布局信息作为一个属性保存在对应的Model中。在UITableView的回调中,可以直接使用Model中的属性,减少了文本的计算。所有在文本渲染画面上可以看到的文本内容控件,包括UIWebView,都是通过CoreText在底层排版绘制成Bitmaps。常用文本控件(UILabel、UITextView等)的排版和绘制都在主线程中进行。当显示大量文字时,CPU的压力会很大。这部分性能优化要求我们放弃使用系统提供的上层控件,直接使用CoreText进行排版控件。尽可能避免更改包含文本的视图的框架,因为这会导致重新绘制文本。例如,如果您需要在经常更改大小的层的角落显示静态文本块,请将文本放在子层中。上面这段话引用自iOSCoreAnimation:AdvancedTechniques。翻译意味着包含文本的视图将在布局更改时触发文本的重新渲染。对于静态文本,我们应该尽量减少对其所在视图的布局修改。ImagedrawingImagedrawing通常是指使用以CG开头的方法将图像绘制到canvas中,然后从中创建并显示图片的过程。帆布。前面的模块图介绍了CoreGraphic作用于CPU,所以调用CG开头的方法会消耗CPU资源。我们可以将绘制过程放在一个后台线程中,然后在主线程中将结果设置为图层的内容。代码如下:-(void)display{dispatch_async(backgroundQueue,^{CGContextRefctx=CGBitmapContextCreate(...);//drawincontext...CGImageRefimg=CGBitmapContextCreateImage(ctx);CFRelease(ctx);dispatch_async(mainQueue,^{layer.contents=img;});});}图像解码加载图像文件后,必须对其进行解压缩。这种减压可能是一项计算复杂的任务,并且需要相当长的时间。解压缩的图像也将使用比原始图像多得多的内存。图片加载完成后,需要进行解码。图片的解码是一个复杂且耗时的过程,需要占用比原始图片更多的内存资源。iOS系统为了节省内存,会延迟解码过程,直到图片被设置到layer的contents属性或者UIImageView的image属性中才会进行解码过程,但这两个操作是在主线程,仍然会带来性能问题。如果想提前解码,可以使用ImageIO或者提前将图片绘制到CGContext中。这部分的练习可以参考《iOSCoreAnimation:AdvancedTechniques》,这里稍微多说一下。常用的UIImage加载方式有imageNamed和imageWithContentsOfFile。其中imageNamed加载图片后会立即解码,系统会将解码后的图片缓存起来,但这种缓存策略是不公开的,图片什么时候发布我们无从知晓。因此,在一些对性能敏感的页面上,我们也可以使用静态变量来保存imageNamed加载的图片,避免被释放,通过空间换时间来提高性能。与CPU相比,GPU消耗的任务相对简单:接收提交的纹理(Texture)和顶点描述(triangle),应用变换(transform),混合渲染,然后输出到屏幕。一般来说,大多数CALayer属性都是使用GPU绘制的。以下操作会降低GPU绘图的性能。大量几何结构的所有Bitmap,包括图片、文字、光栅化内容,最终都会从内存提交到显存,绑定为GPUTexture。无论是提交到显存的过程,还是GPU调整渲染Texture的过程,都会消耗大量的GPU资源。短时间内显示大量图片时(比如TableView中图片很多,快速滑动时),CPU占用率很低,GPU占用率很高,界面还是会掉线帧。避免这种情况的唯一方法就是尽量减少短时间内大量图片的显示,尽可能将多张图片合并为一张显示。另外,当图片过大,超过了GPU的最大纹理尺寸时,图片需要先经过CPU的预处理,这会给CPU和GPU都带来额外的资源消耗。视图的混合当多个视图(或CALayer)重叠显示在一起时,GPU会先将它们混合在一起。如果视图结构过于复杂,混合过程也会消耗大量的GPU资源。在这种情况下,为了减轻GPU开销,应用程序应尽量减少视图的数量和层次结构,并减少不必要的透明视图。离屏渲染离屏渲染是指图层在显示之前,在当前屏幕缓冲区之外的缓冲区中进行渲染。离屏渲染需要多次上下文切换:先从当前屏幕(On-Screen)切换到离屏(Off-Screen);等待离屏渲染完成,将离屏缓冲区的渲染结果显示在屏幕上。将上下文从屏幕外切换到当前屏幕是一项代价高昂的操作。离屏渲染的原因有:shadow(UIView.layer.shadowOffset/shadowRadius/...)圆角(UIView.layer.cornerRadius和UIView.layer.maskToBounds一起使用时)layermaskopenrasterization(shouldRasterize=true)使用shadows时,同时设置shadowPath可以避免离屏渲染,大大提高性能。后面会有Demo来演示;使用CoreGraphics将图片处理成圆角可以避免圆角触发的离屏渲染。CALayer有一个shouldRasterize属性,将这个属性设置为true可以启用光栅化。开启光栅化后,图层会被绘制成一张离屏图像,然后这张图像会被缓存并绘制到实际图层的内容和子图层中。对于具有许多子层或复杂效果的应用程序,这样做会比为所有事务重绘所有帧更高效。但是光栅化原始图像需要时间并消耗额外的内存。光栅化也会带来一定的性能损失。是否启用取决于实际使用场景。图层内容变化频繁时不建议使用。最好用Instruments对比一下开启前后的FPS,看看是否达到了优化效果。注意:当shouldRasterize=true时,记得同时设置rasterizationScaleInstruments。使用Instruments是一系列工具集。我们这里只演示CoreAnimation的使用。在核心动画选项的右下角,您将看到以下选项。ColorBlendedLayers选项将根据渲染级别将屏幕上的混合区域从绿色突出显示为红色。颜色越红,性能越差,会影响帧率等指标。更大的影响。红色通常是由多个半透明层叠加而成。ColorHitsGreenandMissesRed当UIView.layer.shouldRasterize=YES时,耗时的图像绘制将被缓存并呈现为简单的平面图像。此时如果页面其他块(如UITableViewCell的复用)使用缓存直接命中,则显示绿色,反之,未命中则显示红色。红色越多,性能越差。因为光栅化和生成缓存的过程是有开销的,如果缓存能够被大量命中并有效使用,整体成本会降低,否则就意味着必须频繁生成新的缓存,这会使得性能问题更糟。ColorCopiedImages只能对GPU不支持的彩色格式的图片进行CPU处理。用蓝色标记这些图片。蓝色越多,性能越差。颜色立即通常CoreAnimationInstruments每毫秒更新层调试颜色10次。这对于某些效果来说显然太慢了。这个选项可以用来设置每帧更新一次(可能会影响渲染性能,并且会导致帧率测量不准确,所以不要一直设置)。ColorMisalignedImages此选项检查图像是否缩放以及像素是否对齐。缩放后的图像将标记为黄色,未对齐的像素将标记为紫色。黄色和紫色越多,性能越差。ColorOffscreen-RenderedYellow此选项将以黄色显示那些离屏渲染层。越黄,性能越差。这些以黄色显示的图层可能需要使用shadowPath或shouldRasterize进行优化。ColorOpenGLFastPathBlue此选项将以蓝色显示直接使用OpenGL绘制的任何图层。蓝色越多,性能越好。如果你只使用UIKit或者CoreAnimationAPI,是没有效果的。FlashUpdatedRegions选项将以黄色显示重绘的内容。不该出现的黄色越多,性能越差。通常我们只希望更新的部分被标记为黄色。演示上述选项时,通常用于衡量性能的选项是颜色混合图层、屏幕外渲染的黄色和颜色命中绿色和未命中红色。接下来重点演示离屏渲染和光栅化的检测。我写了一个简单的演示来设置阴影效果。代码如下:view.layer.shadowOffset=CGSizeMake(1,1);view.layer.shadowOpacity=1.0;view.layer.shadowRadius=2.0;view.layer.shadowColor=[UIColorblackColor].CGColor;//视图.layer.shadowPath=CGPathCreateWithRect(CGRectMake(0,0,50,50),NULL);在没有设置shadowPath的情况下使用Instruments检测FPS基本在20以下(iPhone6设备),设置shadowPath后基本维持在55左右,性能提升非常明显。我们看一下光栅化的检测,代码如下,view.layer.shouldRasterize=YES;view.layer.rasterizationScale=[UIScreenmainScreen].scale;勾选ColorHitsGreenandMissesRed选项后,显示如下:我们可以看到在缓存静止的时候是有效的,但是快速滑动的时候基本不起作用。所以是否开启光栅化要看具体场景,使用Instruments检测开启前后的性能。小结本文主要总结了性能调优的一些理论知识,然后介绍了CoreAnimation在Instruments中的一些性能检测指标的使用。性能优化最重要的是使用工具来检测而不是猜测。首先检查是否存在离屏渲染等问题,然后使用TimeProfiler分析耗时函数调用。修改后,用工具分析是否有改进,一步一步认真执行。建议大家也手把手分析一下自己的应用加深印象,enjoy~
