当一个产品渐渐成熟,咱们便开始重视产品性能的优化。而这其中图形性能的优化在iOS客户端占比较重要的部分。这里咱们将介绍Core Animation的运行机制,首先咱们不要被它的名字误导了,Core Animation不是只用来作动画的,iOS视图的显示都是经过它来完成的,因此咱们想要优化图形性能必须了解Core Animation。下面咱们根据苹果WWDC视频讲解来认识Core Animation
工做机制,据此分析具体卡顿的缘由,如何避免这些问题形成的卡顿,而且结合实际状况说明从哪些方面优化能够事半功倍。html
Core Animation
在App将图层数据提交到应用外进程Render Server,这是
Core Animation
的服务端,把数据解码成GPU可执行的指令交给GPU执行。能够看出一个问题渲染服务并非在App进程内进行的,也就是说渲染部分咱们没法进行优化,咱们能够优化的点只能在第一个提交事务的阶段。那么这个阶段
Core Animation
到底作了什么呢?下面咱们一块儿来看看!
提交事务分为四个阶段:布局、显示、准备、提交。 ios
根据上面咱们所提到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
上面咱们说过一个混合计算的公式:
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
方法中,设置kCGImageSourceShouldCacheImmediately
为kCFBooleanTrue
的话能够马上开始解压缩,默认为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 Guide和WWDC 2012 Session 238 "iOS App Performance: Graphics and Animations"。字节对齐,在通常状况下,感受对性能的影响很小,不必的状况不要过早优化。
离屏渲染(Off-Screen Rendering)是指GPU在当前屏幕缓冲区之外新开辟一个缓冲区进行渲染操做。离屏渲染是很消耗性能的,由于首先要建立屏幕外缓冲区,还要进行两次上下文环境切换。先切换到屏幕外环境,离屏渲染完成后再切换到当前屏幕,上下文的切换是很高昂的消耗。产生离屏渲染的缘由就是这些图层不能直接绘制在屏幕上,必须进行预合成。
产生离屏渲染的状况大概有几种: 1.cornerRadius
和masksToBounds
(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。能够在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