iOS 图形性能优化

引言

当一个产品渐渐成熟,咱们便开始重视产品性能的优化。而这其中图形性能的优化在iOS客户端占比较重要的部分。这里咱们将介绍Core Animation的运行机制,首先咱们不要被它的名字误导了,Core Animation不是只用来作动画的,iOS视图的显示都是经过它来完成的,因此咱们想要优化图形性能必须了解Core Animation。下面咱们根据苹果WWDC视频讲解来认识Core Animation工做机制,据此分析具体卡顿的缘由,如何避免这些问题形成的卡顿,而且结合实际状况说明从哪些方面优化能够事半功倍。html

Core Animation 工做机制

1.png-c
如上图所示, Core Animation在App将图层数据提交到应用外进程Render Server,这是 Core Animation的服务端,把数据解码成GPU可执行的指令交给GPU执行。能够看出一个问题渲染服务并非在App进程内进行的,也就是说渲染部分咱们没法进行优化,咱们能够优化的点只能在第一个提交事务的阶段。那么这个阶段 Core Animation到底作了什么呢?下面咱们一块儿来看看!

Commit Transaction

提交事务分为四个阶段:布局、显示、准备、提交。 ios

Core Animation.png-c

  • 布局阶段 当调用addSubview时layer被加入到layer tree中,layoutSubviews被调用,建立view。同时还会进行数据查找,例如app作了本地化,label要显示这些本地化字符串必须从本地化文件中查找到对应语言的布局,这就涉及了I/O操做。因此这里主要是CPU工做,而瓶颈也会是CPU。
  • 显示阶段 在这个阶段若是你重写了drawRect方法,Core Graphics会进行绘制渲染工做。为视图绘制寄宿图即contents。可是drawRect里绘制的内容不会当即显示出来,而是先备换窜起来,等须要的时候被更新到屏幕上。如手动调用setNeedsDisplay或sizeThatFits被调用,也能够设置cententMode属性值为UIViewContentModeRedraw当每次bounds改变会自动调用setNeedsDisplay方法。这个阶段主要是CPU和内存的消耗,不少人喜欢用Core Graphics的方法来绘制图形,认为能够提升性能,后面咱们会说明这个方法的弊端。
  • 准备阶段 这里的工做主要是图片的解码,由于大部分都是编码后的图片,要读取原始数据必须通过编码过程。而且当咱们使用了iOS不支持的图片格式,即不支持硬编码,就须要进行转化工做,也是比较耗时的。因此这里就是GPU消耗,若是进行软解码也要消耗CPU。
  • 提交阶段 最后一个阶段负责打包图层数据并发送到咱们上面说的渲染服务中。这个过程是一个递归操做,图层树越复杂越是须要消耗更多资源。像CALaler有不少隐式动画属性也会在这里提交,省去了屡次动画属性进程间的交互,提升了性能。

优化

根据上面咱们所提到4个阶段,咱们看看哪些因素会影响到App的性能,而且如何优化能够提升咱们App的性能。缓存

混合

平时咱们写代码的时候,每每会给不一样的CALayer添加不一样的颜色,不一样的透明度,咱们最后看到是全部这些层CALayer混合出的结果。bash

那么在iOS中是如何进行混合的?前面咱们说明了每一个像素都包含了R(红)、G(绿)、B(蓝)和R(透明度),GPU要计算每一个像素混合来的RGB值。那么如何计算这些颜色的混合值呢?假设在正常混合模式下,而且是像素对齐的两个CALayer,混合计算公式以下:网络

R = S + D * ( 1 – Sa )
复制代码

苹果的文档中有对每一个参数的解释:session

The blend mode constants introduced in OS X v10.5   represent the Porter-Duff blend modes. The symbols in the   equations for these blend modes are:

    * R is the premultiplied result

    * S is the source color, and includes alpha

    * D is the destination color, and includes alpha

    * Ra, Sa, and Da are the alpha components of R, S, and D
复制代码

R就是获得的结果色,S和D是包含透明度的源色和目标色,其实就是预先乘以透明度后的值。Sa就是源色的透明度。iOS为咱们提供了多种的Blend mode:多线程

/* Available in Mac OS X 10.5 & later. R, S, and D are, respectively,
   premultiplied result, source, and destination colors with alpha; Ra,
   Sa, and Da are the alpha components of these colors.

   The Porter-Duff "source over" mode is called `kCGBlendModeNormal':
     R = S + D*(1 - Sa)

   Note that the Porter-Duff "XOR" mode is only titularly related to the
   classical bitmap XOR operation (which is unsupported by
   CoreGraphics). */

kCGBlendModeClear,                  /* R = 0 */
kCGBlendModeCopy,                   /* R = S */
kCGBlendModeSourceIn,               /* R = S*Da */
kCGBlendModeSourceOut,              /* R = S*(1 - Da) */
kCGBlendModeSourceAtop,             /* R = S*Da + D*(1 - Sa) */
kCGBlendModeDestinationOver,        /* R = S*(1 - Da) + D */
kCGBlendModeDestinationIn,          /* R = D*Sa */
kCGBlendModeDestinationOut,         /* R = D*(1 - Sa) */
kCGBlendModeDestinationAtop,        /* R = S*(1 - Da) + D*Sa */
kCGBlendModeXOR,                    /* R = S*(1 - Da) + D*(1 - Sa) */
kCGBlendModePlusDarker,             /* R = MAX(0, (1 - D) + (1 - S)) */
kCGBlendModePlusLighter             /* R = MIN(1, S + D) */
复制代码

彷佛计算也不是很复杂,可是这只是一个像素覆盖另外一个像素简单的一步计算,而正常状况咱们现实的界面会有很是多的层,每一层都会有百万计的像素,这都要GPU去计算,负担是很重的。架构

像素对齐

像素对齐就是视图上像素和屏幕上的物理像素完美对齐。上面咱们说混合的时候,假设的状况是多个layer是在每一个像素都彻底对齐的状况下来进行计算的,若是像素不对齐的状况下,GPU须要进行Anti-aliasing反抗锯齿计算,GPU的负担就会加剧。像素对齐的状况下,咱们只须要把全部layer上的单个像素进行混合计算便可。并发

那么什么缘由形成像素不对齐?主要有两点:app

  1. 图片大小和UIImageView大小不符合2倍3倍关系时,如一张12x12二倍,18x18三倍的图,UIimageView的size为6x6才符合像素对齐。
  2. 边缘像素不对齐,即起始坐标不是整数,可使用CGRectIntegral()方法去除小数位。 这两点都有可能形成像素不对齐。若是想得到更好的图形性能,做为开发者要尽量得避免这两种状况。

不透明

上面咱们说过一个混合计算的公式:

R = S + D * ( 1 – Sa )
复制代码

若是Sa值为1,也就是源色对应的像素不透明。那么获得R = S,这样就只须要拷贝最上层的layer,不须要再进行复杂的计算了。由于下面层的layer所有是可不见的,因此GPU无需进行混合计算了。如何让GPU知道这个图像是不透明的呢?若是使用的是CALayer,那么要把opaque属性设置成YES(默认是NO)。而若只用的是UIView,opaque默认属性是YES。当GPU知道是不透明的时候,只会作简单的拷贝工做,避免了复杂的计算,大大减轻了GPU的工做量。

若是加载一个没有alpha通道的图片,opaque属性会自动设置为YES。可是若是是一个每一个像素alpha值都为100%的图片,尽管此图不透明可是Core Animation依然会假定是否存在alpha值不为100%的像素。

解码

上一篇文章咱们有说到,通常在Core Animation准备阶段,会对图片进行解码操做,即把压缩的图像解码成位图数据。这是一个很消耗CPU的事情。系统是在图片将要渲染到屏幕以前再进行解码,并且默认是在主线程中进行的。因此咱们能够将解码放在子线程中进行,下面简单列举一种解码方式:

NSString *picPath = [[NSBundle mainBundle] pathForResource:@"tests" ofType:@"png"];
NSData *imageData = [NSData dataWithContentsOfFile:picPath];//读取未解码图片数据
        
CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFTypeRef)imageData, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSourceRef, 0, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(NO)});
CFRelease(imageSourceRef);
size_t width = CGImageGetWidth(imageRef);//获取图片宽度
size_t height = CGImageGetHeight(imageRef);//获取图片高度
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
        
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);//每一个颜色组件占的bit数
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);//每一个像素占几bit
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);//位图数据每行占多少bit
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
        
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
CFRelease(imageRef);
CFDataRef dataRef = CGDataProviderCopyData(dataProvider);//得到解码后数据
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(dataRef);
CFRelease(dataRef);
        
CGImageRef newImageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
CFRelease(newProvider);
        
UIImage *image = [UIImage imageWithCGImage:newImageRef scale:2.0 orientation:UIImageOrientationUp];
CFRelease(newImageRef);
复制代码

另外,在iOS7以后苹果提供了一个属性kCGImageSourceShouldCacheImmediately,在CGImageSourceCreateImageAtIndex方法中,设置kCGImageSourceShouldCacheImmediatelykCFBooleanTrue的话能够马上开始解压缩,默认为kCFBooleanFalse。固然也像AFNetworking 中使用void CGContextDrawImage(CGContextRef __nullable c, CGRect rect, CGImageRef __nullable image)方法也能够实现解码,具体实现不在此赘述。

字节对齐

咱们前面说像素对齐时,简单介绍了字节对齐。那么到底什么是字节对齐?为何要字节对齐?和咱们优化图形性能有什么关系呢?

字节对齐是对基本数据类型的地址作了一些限制,即某种数据类型对象的地址必须是其值的整数倍。例如,处理器从内存中读取一个8个字节的数据,那么数据地址必须是8的整数倍。

对齐是为了提升读取的性能。由于处理器读取内存中的数据不是一个一个字节读取的,而是一块一块读取的通常叫作cache lines。若是一个不对齐的数据放在了2个数据块中,那么处理器可能要执行两次内存访问。当这种不对齐的数据很是多的时候,就会影响到读取性能了。这样可能会牺牲一些储存空间,可是对提高了内存的性能,对现代计算机来讲是更好的选择。

在iOS中,若是这个图像的数据没有字节对齐,那么Core Animation会自动拷贝一份数据作对齐处理。这里咱们能够提早作好字节对齐。在方法CGBitmapContextCreate(void * __nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef __nullable space, uint32_t bitmapInfo)中,有一个参数bytesPerRow,意思是指定要使用的位图每行内存的字节数,ARMv7架构的处理器的cache lines是32byte,A9处理器的是64byte,这里咱们要使bytesPerRow为64的整数倍。具体能够参考官方文档Quartz 2D Programming GuideWWDC 2012 Session 238 "iOS App Performance: Graphics and Animations"。字节对齐,在通常状况下,感受对性能的影响很小,不必的状况不要过早优化。

离屏渲染

离屏渲染(Off-Screen Rendering)是指GPU在当前屏幕缓冲区之外新开辟一个缓冲区进行渲染操做。离屏渲染是很消耗性能的,由于首先要建立屏幕外缓冲区,还要进行两次上下文环境切换。先切换到屏幕外环境,离屏渲染完成后再切换到当前屏幕,上下文的切换是很高昂的消耗。产生离屏渲染的缘由就是这些图层不能直接绘制在屏幕上,必须进行预合成。

产生离屏渲染的状况大概有几种: 1.cornerRadiusmasksToBounds(UIView中是clipToBounds)一块儿使用的时候,单独使用不会触发离屏渲染。cornerRadius只对背景色起做用,因此有contents的图层须要对其进行裁剪。 2.为图层设置mask(遮罩)。 3.layer的allowsGroupOpacity属性为YES且opacity小于1.0,GroupOpacity是指子图层的透明度值不能大于父图层的。 4.设置了shadow(阴影)。

上面这几种状况都是GPU的离屏渲染,还有一种特殊的CPU离屏渲染。只要实现Core Graphics绘制API会产生CPU的离屏渲染。由于它也不是直接绘制到屏幕上的,并且先建立屏幕外的缓存。

咱们如何解决这几个产生离屏渲染的问题呢?首先,GroupOpacity对性能几乎没有影响,在此就很少说了。圆角是一个没法避免的,网上有不少例子是用Core Graphics绘制来代替系统圆角的,可是Core Graphics是一种软件绘制,利用的是CPU,性能上要差上很多。固然在CPU利用率不是很高的界面是个不错的选择,可是有时候某个界面可能须要CPU去作其余消耗很大的事情,如网络请求。这个时候时候在用Core Graphics绘制大量的圆角图形就有可能出现掉帧。这种状况怎么办呢?最好的就是设计师直接提供圆角图像。还有一种折中的方法就是在混合图层,在原图层上覆盖一个你要的圆角形状的图层,中间须要显示的部分是透明的,覆盖的部分和周围背景一致。

对于shadow,若是图层是个简单的几何图形或者圆角图形,咱们能够经过设置shadowPath来优化性能,能大幅提升性能。示例以下:

imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
复制代码

咱们还能够经过设置shouldRasterize属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么很差,为何咱们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要从新合成这些图层,十分消耗性能。当咱们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。可是若是图层发生改变的时候就会从新产生位图缓存。因此这个功能通常不能用于UITableViewCell中,cell的复用反而下降了性能。最好用于图层较多的静态内容的图形。并且产生的位图缓存的大小是有限制的,通常是2.5个屏幕尺寸。在100ms以内不使用这个缓存,缓存也会被删除。因此咱们要根据使用场景而定。

Instruments

上面咱们说了这么多性能相关的因素,那么咱们怎么进行性能的测试,怎么知道哪些因素影响了图形性能?苹果很人性得为咱们提供了一个测试工具Instruments。能够在Xcode->Open Develeper Tools->Instruments中找到,咱们看到这里面有不少的测试工具,像你们可能经常使用的检测内存泄漏的Leaks,在这里咱们就讨论下Core Animation这个工具的使用。

Core Animation工具用来监测Core Animation性能。提供可见的FPS值。而且提供几个选项来测量渲染性能,下面咱们来讲明每一个选项的能: Color Blended Layers:这个选项若是勾选,你能看到哪一个layer是透明的,GPU正在作混合计算。显示红色的就是透明的,绿色就是不透明的。

Color Hits Green and Misses Red:若是勾选这个选项,且当咱们代码中有设置shouldRasterize为YES,那么红色表明没有复用离屏渲染的缓存,绿色则表示复用了缓存。咱们固然但愿可以复用。

Color Copied Images:按照官方的说法,当图片的颜色格式GPU不支持的时候,即不是32bit的颜色格式,Core Animation会 拷贝一份数据让CPU进行转化。例如从网络上下载了8bit的颜色格式的图片,则须要CPU进行转化,这个区域会显示成蓝色。还有一种状况会触发Core Animation的copy方法,就是字节不对齐的时候。

Color Misaligned Images:勾选此项,若是图片须要缩放则标记为黄色,若是没有像素对齐则标记为紫色。像素对齐咱们已经在上面有所介绍。

Color Offscreen-Rendered Yellow:用来检测离屏渲染的,若是显示黄色,表示有离屏渲染。固然还要结合Color Hits Green and Misses Red来看,是否复用了缓存。

Color OpenGL Fast Path Blue:这个选项对那些使用OpenGL的图层才有用,像是GLKView或者 CAEAGLLayer,若是不显示蓝色则表示使用了CPU渲染,绘制在了屏幕外,显示蓝色表示正常。

Flash Updated Regions:当对图层重绘的时候回显示黄色,若是频繁发生则会影响性能。能够用增长缓存来加强性能。官方文档Improving Drawing Performance有所说明。

总结

结合前面两章内容,咱们发现,一个简单的图片显示在屏幕上,要通过不少步骤,而且有许多硬件的参与。最主要的就是CPU和GPU,协调他们之间的工做是高性能得关键。

由于图形的性能和二者都有关系,CPU主要负责软解码、I/O相关、布局的计算等工做,若是使用Core Graphics绘图API那么也会用到CPU。GPU的主要责任就是合成渲染。为了可以获得最好的性能,咱们就要找出是哪一个限制了性能,CPU过分利用仍是GPU负担太大。经过苹果给出的Instruments里面的测试工具,咱们在真机上一次次的测试,才能正确的判断出没法保证画面60FPS的缘由。必须平衡二者,才能达到最好的性能。

下面咱们总结几个优化点: 1.尽可能使用iOS优化处理的图片格式,减小CPU软解码的负担。 2.能不透明的不要使用透明度,减小混合计算。 3.不要让图层过于复杂,否则增长了处理图层,打包传送到渲染服务的工做量,GPU渲染负担也会增大。 4.最好不要使用离屏渲染,必须使用的话最好可以复用缓存,离屏渲染对性能影响是最大的。 5.布局不要过于复杂,若是必需要复杂的布局,能够提早缓存布局数据。 6.不要滥用多线程,由于建立和销毁线程不只增长CPU任务量,并且会消耗内存。

最后须要说明的就是不要过早和过分得优化,过犹不及。过早优化得不偿失,反而耗时耗力。过分优化有时候拔苗助长。

视频参考:WWDC 2015’s session 233:Advanced Touch Input on iOS

文档参考:objccn

相关文章
相关标签/搜索