1

本文来自于Dev Club 开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57fc8...

使用多张图片做帧动画的性能优化

背景

QQ群的送礼物功能需要加载几十张图然后做帧动画,但是多张图片加载造成了非常大的性能开销,导致图片开始加载到真正播放动画的时间间隔比较长。所以需要研究一些优化方案提升加载图片和帧动画的性能。

原理分析

iOS系统从磁盘加载一张图片,使用UIImageView显示到屏幕上,需要经过以下步骤:

  1. 从磁盘拷贝图片数据到内核缓冲区。

  2. 从内核缓冲区复制数据到用户空间。

  3. 生成UIImageView,把图像数据赋值给UIImageView。

  4. 如果图像数据为未解码的PNG/JPG,解码为位图数据。

  5. CATransaction捕获到UIImageView layer树的变化,主线程Runloop提交CATransaction,开始进行图像渲染。如果数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐。GPU处理位图数据,进行渲染。

加载图片

加载图片和图片解码是比较容易影响性能的一些因素。iOS设备上的闪存虽然是非常快的,但是还是要比内存慢接近200倍左右。图片加载的性能取决于CPU和IO,所以适当的减少IO次数可以提升一部分性能。

通常情况下,应用应该在用户不会察觉的时候加载图片,可以针对情况采用预加载图片或者延迟加载。如果是帧动画这种情况,图片很难做延迟加载,因为帧动画的时间比较短,延迟加载很难保证播放每一帧时能提前加载到图片。有些比如列表滑动之类的情况延迟加载就会比较合理,并且可以采用子线程异步之类的方法防止滑动卡顿。

解码

图片加载结束之后在被渲染到屏幕之前,如果是未解码的JPEG或者PNG格式,图片会先被解码为位图数据。经过实际的测试,图片解码通常要比图片加载耗费更多的时间。iOS默认会在主线程对图像进行解码。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。

解码与加载图片耗时对比

测试机型:iPhone 6+

--- 只加载不解码平均时长 加载和解码平均时长
同一张图加载三十次 0.000858531 0.005955906
加载三十张不同的图 0.002828871 0.015458194

解码的时间通常是加载图像数据时间的三到四倍左右。

各种加载图片API的解码的时机

UIKit:

+imageNamed://加载到原图后立刻进行解码
+imageWithContentsOfFile://图像渲染前进行解码

UIImageView的image被赋值时会立刻进行解码。

图像用UIKit内的绘图API绘制时会立刻进行解码,这个API的好处是可以在子线程进行。

ImageIO:

NSURL *imageURL = [NSURL fileURLWithPath:str];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES, (__bridge id)kCGImageSourceShouldCacheImmediately: @NO};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);

kCGImageSourceShouldCacheImmediately 决定是否会在加载完后立刻开始解码。

缓存

缓存分为原图像的缓存和解码后位图数据的缓存。
一般情况下,解码后的位图数据的缓存会跟随着原图UIImage,并且UIImage被拷贝后,指针变了之后图像需要重新去解码。

解码后的位图数据可以通过以下的API获取。

1.CGImageSourceCreateWithData(data) 创建 ImageSource。
2.CGImageSourceCreateImageAtIndex(source) 创建一个未解码的 CGImage。
3.CGImageGetDataProvider(image) 获取这个图片的数据源。
4.CGDataProviderCopyData(provider) 从数据源获取直接解码的数据。
各种加载图片API的缓存策略

UIKit:

+imageNamed://原图和解码后的位图数据都会保存在系统缓存下,只有在内存低之类的时候才会被释放。所以这个API适合在应用多次使用的图片上使用。
+imageWithContentsOfFile://不会对原图做缓存,只用一次的图片应该使用这个API。

ImageIO:

ImageIO的API的kCGImageSourceShouldCache选项决定了是否会对解码后的位图数据做缓存。64位设备上默认为开,32位设备上默认为关。

任何时候,选取如何缓存总是一件比较难的事,正如菲尔 卡尔顿曾经说过:“在计算机科学中只有两件难事:缓存和命名”。

解码后位图数据的缓存不好控制,我们一般也不会去操作它,但是原图像还是可以做一些缓存的。除了使用系统API来做缓存以外,应用也可以自定义一些缓存策略。或者可以使用NSCache。

NSCache的API和NSDictionary很像,并且会根据所保存数据的使用频率,内存占用情况会适时的释放掉一其中一部分数据。

图片格式

常用的图片格式一般分为PNG和JPEG。

对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

JPEG 对于噪点大的图片效果比较好,PNG适合锋利的线条或者渐变色的图片。对于不友好的png图片,相同像素的JPEG图片总是比PNG加载更快,除非一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的

本文测试时使用的所有图片均为PNG格式。

渲染

关于图像的渲染,主要从以下三点分析:

  • offscreen rendring

  • Blending

  • Rasterize

Offscreen rendering指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。在进行offscreen rendring的时候,显卡需要另外alloc一块内存来进行渲染,渲染完毕后在绘制到当前屏幕,而且对于显卡来说,onscreen到offscreen的上下文环境切换是非常昂贵的(涉及到OpenGL的pipelines和barrier等),

会造成offscreen rendring的操作有:

  • layer.mask 的使用

  • layer.maskToBounds 的使用

  • layer.allowsGroupOpacity 设置为yes 和 layer.opacity 小于1.0

  • layer.shouldRasterize 设置为yes

  • layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing

Blending 会导致性能的损失。在iOS的图形处理中,Blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。更多的计算,导致性能的损失,在一些不需要透明度的地方,可以设置alpha为1.0 或者减少图层的叠加。

Rasterize启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。
当我们使用得当时,光栅化可以提供很大的性能优势但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。

优化点

加载

应用可以通过减少IO次数来优化图像加载性能。这时候就可以使用精灵序列来把多张小图合成一张大图。 这样就能极大的减少IO次数了。

因为IOS设备的限制,大图的大小不能超过2048 2048,有部分设备上这个限制可以达到4096 4096,但我们这里以小的为准。所以一张大图内最多只能容纳10张测试用的小图。

精灵序列这种技术在cocos2d-x等平台上使用的很多。把多张小图拼成一张大图时可以使用TexturePacker软件来完成。

精灵序列

<img src="http://ww2.sinaimg.cn/mw690/6... height="291" width="258.3"/>

简单来说精灵序列就是把多张小图拼成一张大图,附加一份plist文件保存着每一张小图对应在大图上的位置。然后iOS上就可以通过如下代码在不同小图之间切换。

layer.contents = (id)img_.CGImage;//img为对应的大图
layer.contentsRect = CGRectMake(0.1, 0.1, 0.2, 0.2);//contentsRect就是对应大图上小图的位置,更改这个值就可以在不同的小图间切换了。
精灵序列与普通帧动画的性能对比

下面的性能对比是通过20张小图和小图拼接而成的2张大图完成一组动画的对比数据。 测试机型是iPhone4s.

1.文件大小

小图总大小 大图总大小
758K 827K

因为大图内很难平铺所有小图,所以大图内很容易有空白像素,这就导致拼接后大图的总分辨率很容易超过所有小图的总分辨率,也就造成大图的体积会比所有小图的体积大。但是也有大图的体积小于所有小图总体积的情况,因为TexturePacker在拼接的时候会把小图内空白的像素适当的做点裁剪,然后把这个偏移值保存在plist文件内。

2.加载速度

小图的加载时间 大图的加载时间
≈25ms ≈5ms

一张大图能容纳10张小图,极大的减小了IO数量,所以加载速度能大大提升。

3.解码时间

小图的解码时间 大图的解码时间
≈35ms ≈40ms

因为大图的数据量比较大,所以大图的总体解码时间要比小图的解码时间长。

4.CPU占用(CPU占用是通过同时执行两种动画,然后分别计算出两种动画的CPU占用率)

小图方案的CPU占用率 大图方案的CPU占用率
≈8% 峰值比较高 ≈20% 峰值比较低

占用CPU最多的一个是从本地加载数据,另一个是图片解码。因为大图的IO次数比较少,所以加载大图时的CPU占用率要比加载所有小图时的低,但是大图的解码和两张大图切换时比较耗费CPU。总体来说大图这种方案的CPU占用率要高一点。

5.内存占用

小图方案的内存占用 大图方案的内存占用
≈27MB ≈30MB

6.帧率

小图方案的帧率 大图方案的帧率
≈7fps ≈7fps

大图方案和小图方案的帧率基本一致。

精灵序列总结

总体来说精灵序列这种方案能明显减小图片开始加载到动画开始播放的延迟。但是精灵序列的文件大小容易变大,并且CPU占用率也要高一点。

-- 文件大小 加载速度 解码时间 CPU占用 内存占用
通过大图实现的精灵序列
通过小图实现的普通帧动画

精灵序列这种方案或许也可以直接用解码后的数据作为原图数据,这样虽然会让内存占用更高,文件大小进一步加大,但是能降低解码时间和CPU占用率。这个可以作为一个优化点继续研究一下。

精灵序列的风险点:

  1. 因为一张大图最多能容纳10张小图,所以在两张大图切换的时候会造成CPU占用变高,这样就会有造成卡顿的风险。但是在iPhone4s上的测试下基本没有出现过卡顿。

  2. 拼接后的大图的文件大小容易变大,如果大图通过网络下载时耗时会变长。

延迟解码

根据上文的原理分析,针对多图帧动画,应用可以将图片解码延迟到图片渲染前,不要让图片加载后立马开始解码。这样也能降低图片加载到动画开始播放的延迟。

缓存

因为QQ群的送礼物功能中同一副帧动画重复播放的频率并不高,所以不需要考虑对原图或者对解码后位图数据做缓存。

渲染

渲染图形方面应用只能在将需要绘制的内容提交给GPU前做一些优化。针对帧动画这种场景,我们可以通过保证图像素材做到像素对其,尽量减少透明像素来做一些优化。

如果使用精灵序列来做帧动画,应用必须通过CALayer进行渲染。播放帧动画时可以用NSTimer也可以使用CADisplayLink来不断的刷新图像数据。

如果不使用精灵序列,应用也可以通过自定义一个UIImageView,自己通过CADisplayLink或者NSTimer实现帧动画,这样的话应用就可以自由控制图像解码的时间,图像数据的缓存等。

总结

本文通过对图片加载,多图做帧动画进行了一些原理分析,并且给出了一些优化点。也简单介绍了一下用精灵序列做帧动画的方案。除了QQ群送礼物的功能外其他通过图片做帧动画的功能也可以针对具体的业务情况选取其中的一些优化点进行一些优化。

更多精彩内容欢迎关注腾讯优测的微信公众账号:

腾讯优测是专业的移动云测试平台,为应用、游戏,H5混合应用的研发团队提供产品质量检测与问题解决服务。不仅在线上平台提供「全面兼容测试」、「云手机」等多种质量检测工具,同时在线下为VIP客户配备专家团队,提供定制化综合测试解决方案。真机实验室配备上千款手机,覆盖亿级用户,7*24小时在线运行,为各类测试工具提供支持。


腾讯Bugly
2.8k 声望718 粉丝