深入分析iOS应用中对于图片缓存的管理和使用
我们的 iOS 应用都包含了大量的图像。创建富有吸引力的视图,主要依赖于大量的装饰图片,所有这些首先必须从远程服务器获取。如果每次打开应用都要从服务器一次又一次的获取每个图像,那么用户体验肯定达不到好的效果,所以本地缓存远程图像是非常有必要的。
两种方式加载本地图片
1.通过imageNamed:方法加载图片
用过这种方式加载图片,一旦图片加载到内存中,那么就不会销毁,一直到程序退出。(也就是说imageNamed:会有图片缓存的功能,当下次访问图片的时候速度会更快。)
用这种方式加载图片,图片的内存管理并不受程序员控制。
UIImage *image = [UIImage imageNamed: @“image”]
的意思是创建一个UIImage对象,并不是说image这个本身就是一张图片,而是image指向一张图片。在创建这个对象的时候实际上并没有把真正的图片加载到内存里,而是等到用到图片的时候才会加载。
如上例,如果把image对象设置为nil,如果是其它对象,那么没有强指针指向一个对象,这个对象就会销毁;但是即使image = nil,它会指向的图片资源也不会销毁。
2.通过imageWithContentsOfFile:方式加载图片
使用这个方法加载图片,当指向图片对象的指针销毁或指向其它对象,这个图片对象没有其它强指针指向,这个图片对象会销毁,不会一直在内存中停留。
因为没有缓存,所以如果相同的图片多次加载,那么也会有多个图片对象来占用内存,而不是用缓存的图片。
使用这个方法,需要file的全路径(之前用NSString, NSArray之类的加载文件也是一样的,比如stringWithContentsOfFile:,看到file就知道是需要传入全路径。)
NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
注意如果图片在Images.xcassets中,是不能使用这个方法的。所以说想要自己进行图片的内存管理(不希望有缓存图片),那么要将图片资源直接拖入工程,而不是放在Images.xcassets中。
快速队列和慢速队列
我们设置了两个队列,一个串行,一个并行。在屏幕上被迫切要求的图片进入并行队列(fastQueue),可能晚点才需要的图片进入串行队列(slowQueue)。
就UITableView的实现而言,这意味着在屏幕上的表格单元从fastQueue获取图片, 每个关闭的屏幕行的图片从slowQueue预加载。
现在不需要处理图片
假设我们要从服务器上请求包含30条事件的一页资讯回来,一旦这些内容请求回来时我们就可以排队等待预取其中的每一张图。
- (void)pageLoaded:(NSArray *)newEvents {
for (SGEvent *event in newEvents) {
[SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];
}
}
slowGetImageForURL:这个方法将图片添加到slowQueue这个队列当中,允许它们在不阻塞网络通信的前提下被一张一张的取出来。
thenDo:这个代码块在这里是没有被实现,是因为我们目前还不需要对图片做任何事情。所有我们需要做的就是确保它们在本地磁盘缓存当中,并且随时准备在屏幕上滑动表格时来使用。
现在就要处理图片
显示在屏幕上的表格希望立即显示它们的图片,所以在table cell子类当中实现:
- (void)setEvent:(SGEvent *)event {
__weak SGEventCell *me = self;
[SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) {
me.imageView.image = image; }
];
}
getImageForURL:这个方法将抓取图片的过程添加到fastQueue这个队列当中,意味着只要iOS系统允许,它们会并行被地执行。如果抓取图片的过程已经存在于slowQueue队列当中,它会被移动到fastQueue队列中,从而避免重复请求。
一直异步
等等,getImageForURL:不是一个异步方法吗?如果你明知道图片已经在缓存中,但是却不想在主线程上立即使用它吗?直觉告诉你那是错误的。
从磁盘上加载图片太费资源,同样解压图片也会费很多资源。可以在滑动的过程当中进行配置和添加表格,这最后一件你想在滑动表格时做的事是很危险地,因为它会阻塞主线程,会有卡顿的现象出现。
使用getImageForURL:可以让磁盘加载的动作脱离主线程,于是当thenDo:这个用于收尾工作的代码块执行的时候它已经有了一个UIImage实例,从而不会有滑动卡顿的危险。如果图片已经存在于本地缓存当中,用于收尾工作的代码块会在下一次运行周期执行,并且用户不会注意到两者之间的差别。他们会注意到的是滑动不会卡顿了。
现在,不需要你快速执行
如果用户很快的滑动表格到底部,几十或几百个表格单元会出现在屏幕上,并向fastQueue请求图片数据,然后很快地从屏幕上消失。突然间这个并行地队列会将大量实际上不再需要的图片请求充斥进网络。当用户最终停止滑动时,那些当前屏幕上相应的表格单元视图会将它们的图片请求至于那些并不急需的请求后面,因此网络阻塞了。
这就是 wheremoveTaskToSlowQueueForURL:这个方法的产生的原因.
// a table cell is going off screen-
(void)tableView:(UITableView *)table
didEndDisplayingCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath*)indexPath {
// we don't need it right now, so move it to the slow queue
[SGImageCache moveTaskToSlowQueueForURL:[[(id)cell event] imageURL]];
}
这确保在fastQueue中的只会有真正需要被快速执行的任务。任何以前认为需要快速执行但现在不需要的任务会被移至slowQueue中。
重点和选择
已经有相当多的iOS图片缓存库。它们中一些库只针对某些应用场景,一些库提供了不同场景一定的可扩展性。我们的库即没有专门针对某些应用场景,也没有太多大而全的特性。针对我们的用户我们有三类基本的重点:
重点 1: 最好的帧率
很多的库都非常专注在这一点上,使用一些高度定制和复杂的方法,尽管基准没有决定性地显示这样有效。我们发现最好的帧率由这些决定:
将对磁盘的访问(并且几乎其它的所有)脱离主线程。
使用UIImage的内存缓存来避免不必要的磁盘访问和图片解压。
重点 2: 让最最重要的图片优先显示
大多数的库都考虑让队列管理成为别人关心的事。对于我们的应用,这几乎是最重要的点。
让正确的图片在正确的时间显示在屏幕上可以归结为一个简单的问题:“我们现在就需要它显示还是过一会儿?”。那些需要立即显示的图片是并行加载地,而其它所有东西都被添加到串行队列中。所有之前急迫的事但现在不急迫的话就会从fastQueue分到slowQueue中。并且当fastQueue在工作时,slowQueue是处于挂起状态的。
这让那些急需显示的图片可以单独访问网络,同时也确保了一张非急需显示的图片可以在过一会成为一张急需显示的图片,因为它已经存到了缓存当中,随时准备用于显示。
重点 3: 尽可能简单的API
大多数库都做到了这一点。许多库为了隐藏细节内容而提供了UIImageView的分类,并且许多库让抓取一张图片的流程变得尽可能的便利。针对我们经常做的三件事,我们的库选定了三个主要的方法:
快速抓到一张图
__weak SGEventCell *me = self;[SGImageCache getImageForURL:event.imageURL thenDo:^(UIImage *image) { me.imageView.image = image;}];
排队等待一张我们一会才需要的图片
[SGImageCache slowGetImageForURL:event.imageURL thenDo:nil];
通知缓存一张急需显示的图已经不需要立刻显示
[SGImageCache moveTaskToSlowQueueForURL:event.imageURL];
结论
通过专注于预取,队列管理,从主线程移除耗时的任务,并且依赖于UIImage内置的内存缓存,我们努力从一个简单的软件包中得到好的结果。