帧动画的图片内存管理小记

背景

最近在做有关帧动画的一个小玩意儿。需求是:一共有几套帧动画,我可以随时在几套帧动画之间进行切换。

Step By Step

在最开始做的时候,我们是把帧动画的每一帧都使用[UIImage imageNamed:@"xxx"]塞进NSArray然后一股脑地丢给了UIImageView的animationImages

只需要把图片数组传给UIImageView的animationImages,再设置一下animationDuration等等就可以调用starAnimation播放动画了~~

由于我们的动画有好几套,互相切换后,导致图片占用的内存空间相当之大。一不小心乱搞搞就crash了。后来在同事的提醒下,发现了以下二者的细微差别:

+ (UIImage *)imageNamed:(NSString *)name
+ (UIImage *)imageWithContentsOfFile:(NSString *)path

二者虽然都可以获取到图片,但底层的处理是不一样的。来,上官方文档上面的解释:

对于imageNamed方法

This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method locates and loads the image data from disk or asset catelog, and then returns the resulting object.

...

If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image using imageWithContentsOfFile:. This will keep your single-use image out of the system image cache, potentially improving the memory use characteristics of your app.

翻译一下:如果所要使用的图片对象存在,此方法可通过具体的名字从系统缓存中找到对应的图片对象。如果一个相匹配的图片对象还没有被存在缓存之中,这个方法就会从磁盘或者asset中寻找并加载到图片数据,然后返回。

...

如果你的图片文件只需要展示一遍,并且希望保证它不被加入系统缓存,你应该使用imageWithContentsOfFile方法来获取图片。这样才会保证只会使用一次的图片不在系统缓存之中,从而降低内存使用率。

对于imageWithContentsOfFile方法

This method does not cache the image object.

很简单,那就是不会缓存。

其实像这样的不会加入到系统缓存中的方法还很多,文档上面一大堆。这次遇到的难题总算是得到了解决,我们可以自由地控制图片的内存使用,合适地释放掉它。

着手优化内存

在发现imageNamed方法居然有提供系统缓存后,我觉得Apple真不赖,顺便帮做缓存,于无形之中优化了不少重复使用图片时的内存效率,真是赞!「然而」,我特么没有发现Apple有提供给我们释放系统缓存的接口,我了个大去!既然这样,只能手动管理内存了。

我们第一个想到的是:用完就释放

我们项目中有几套帧动画需要互相切换,那么在切换后,以前使用而现在不需要使用的图片数据就不需要保留了,直接设为nil~释放掉。这样,结合我们对动画帧的控制,就可以很好的将内存消耗限制在100M之内甚至更低。

但是,问题又来了。我们还有个需求是在一个界面上可能会同时出现两套一样的图片,这个时候,却又有两个拥有同样数据的图片数组。我们想到是否还可以进一步地优化---即对同时出现相同数据的内存进行进一步地优化。

进一步的优化:自己维护一套类似于引用计数的东西

说干就干:

  1. 首先给Character对象(也就是帧动画数据存放的对象)设计了一个hash函数,用于判断两个对象所持有的数据是否一致。
  2. 在CharacterManager对象中,加了个字典用于持有Character对象以及retainCount。
  3. Character对象init时,retainCount++。dealloc时retainCount--。若retainCount为0了,那么就直接把Character对象置为nil。
  4. 在Character对象init时还未被CharacterManager持有的话,就将其持有,并设置retainCount。

然而,事情并没有想象中那样开心

经过多次检测,我们发现,这样并! 没! 有! 什! 么! 卵! 用!

是的,几乎没有改进

百思不得其解啊喵>_<

后来想想,大概是因为图片在绘制到屏幕上的时候,就该占用那么多的内存。因为一个像素点就占用了4个字节(r g b a),我无论怎么优化,也不能说把这块节省下来。

最后

但确实学到了不少,我在ARC下实现了引用计数2333(真是奇葩),还了解到了imageNamed与系统缓存之间的暧昧关系。以后再遇到这种类似的问题,希望能少踩一点坑。

后记 9.16

在上面提到了自己手动撸了个字典对引用计数以及图片等等进行存储。但在我刚刚看『Effective Objective-C 2.0』一书中提到了NSCache相关的东西,我决定好好看一下后,在下一篇文章中和大家一起探讨一下NSCache :]