本文将详细解释iOS如何优化图形性能。边肖觉得很实用,就分享给大家参考。希望你看完这篇文章能有所收获。
引言
当一个产品成熟后,我们开始关注产品性能的优化。其中,图形性能的优化是iOS客户端的重要组成部分。
这里我们将介绍核心动画的运行机制。首先,我们不要被它的名字误导。核心动画不仅用于动画,还用于显示iOS视图。因此,如果我们想要优化图形性能,就必须了解核心动画。
让我们根据苹果WWDC的视频讲解来了解Core Animation的工作机制,分析停滞的具体原因,如何避免这些问题带来的停滞,并结合实际情况从哪些方面说明优化可以起到事半功倍的效果。
Core Animation 工作机制
如上图所示,Core Animation将图层数据提交给app中的应用外进程Render Server,它是Core Animation的服务器,并将数据解码成GPU可执行的指令来执行。
可以看出,App流程中并没有进行一个问题渲染服务,也就是说我们无法对渲染部分进行优化,能够优化的点只能在事务提交的第一阶段。那么核心动画在这个阶段做了什么呢?我们一起来看看吧!
Commit Transaction
提交事务有四个阶段:布局、显示、准备和提交。
布局阶段
调用addSubview时,图层会添加到图层树中,并调用layoutSubviews来创建视图。同时,将进行数据搜索。比如app是本地化的,label必须从本地化的文件中找到对应语言的布局才能显示这些本地化的字符串,这就涉及到I/O操作。所以这里工作的主要是CPU,瓶颈也会是CPU。
显示阶段
在这个阶段,如果你重写drawRect方法,Core Graphics将渲染并绘制。为视图(即内容)绘制宿主图。但是,drawRect中绘制的内容不会立即显示,而是在需要时被替换并更新到屏幕上。如果手动调用setNeedsDisplay或sizeThatFits,还可以将cententMode属性值设置为UIViewContentModeRedraw,并且每次边界发生变化时都会自动调用setNeedsDisplay方法。这个阶段主要是CPU和内存的消耗。很多人喜欢用Core Graphics绘制图形,这样可以提高性能。我们将在后面解释这种方法的缺点。
准备阶段
这里的主要工作是图片的解码,因为大部分都是编码图片,原始数据必须编码。而当我们使用iOS不支持的图像格式,也就是不支持硬编码的时候,就需要做转换工作,这也是很费时间的。这里是GPU消耗。如果进行软解码,也会消耗CPU。
提交阶段
最后一个阶段负责打包图层数据并将其发送给上面提到的渲染服务。这个过程是递归操作,层树越复杂,需要消耗的资源就越多。这里还会提交很多像CALaler这样的隐式动画属性,节省了很多动画属性过程之间的交互,提高了性能。
优化
根据上面提到的四个阶段,让我们看看哪些因素会影响App的性能,如何优化才能提高我们App的性能。
混合
平时写代码的时候,经常会给不同的CALLAYERS添加不同的颜色和不同的透明度。最后,我们看到了混合所有这些层的结果。
那么在iOS中是如何混合的呢?我们前面解释过,每个像素包含R(红色)、G(绿色)、B(蓝色)和R(透明度),GPU应该计算每个像素的RGB值。那么如何计算这些颜色的混合值呢?假设在正常混合模式下,有两个像素对齐的CALayer,混合计算公式如下:
r=s d *(1sa)苹果的文档对每个参数都有解释:
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上的单个像素进行混合计算即可。
那么什么原因造成像素不对齐?主要有两点:
-
图片大小和UIImageView大小不符合2倍3倍关系时,如一张12x12二倍,18x18三倍的图,UIimageView的size为6x6才符合像素对齐。
-
边缘像素不对齐,即起始坐标不是整数,可以使用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方法中,设置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在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。离屏渲染是很消耗性能的,因为首先要创建屏幕外缓冲区,还要进行两次上下文环境切换。先切换到屏幕外环境,离屏渲染完成后再切换到当前屏幕,上下文的切换是很高昂的消耗。产生离屏渲染的原因就是这些图层不能直接绘制在屏幕上,必须进行预合成。
产生离屏渲染的情况大概有几种:
-
cornerRadius和masksToBounds(UIView中是clipToBounds)一起使用的时候,单独使用不会触发离屏渲染。cornerRadius只对背景色起作用,所以有contents的图层需要对其进行裁剪。
-
为图层设置mask(遮罩)。
-
layer的allowsGroupOpacity属性为YES且opacity小于1.0,GroupOpacity是指子图层的透明度值不能大于父图层的。
-
设置了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(https://developer.apple.com/library/archive/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/DrawingTips/DrawingTips.html)有所说明。
关于“iOS如何实现图形性能优化”这篇文章就分享到这里了,希望
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/157460.html