写在前面
“下载图片”几乎是每一个移动App都要处理的问题,对于iOS开发平台而言,下载图片并不是一个多么复杂的事情,给定一个URL,然后使用URL相关库(譬如AFNetworking)去把图片取出来即可,但站在用户的角度,相对于文本信息,下载图片往往会带来更大的成本(下载时间长,流量大等),所以仍然有不少问题需要开发者考虑,譬如本地缓存、URL缓存、服务端压缩与客户端解压等,有些时候还有性能的考量。
笔者目前接触的App开发不是特别多,对性能要求不是特别高,所以着重需要考虑图片流量问题,简而言之,所需要做的事情是尽可能少地通过网络从服务端下载图片,所以本文主要谈谈图片缓存机制。
笔者长时间使用的图片缓存框架是UIImageView(AFNetworking),但并没有直接使用,而是根据自己的需要加以修改,使之更加完善;另外一种倍受好评且使用广泛的的图片缓存框架是SDWebImage,本文也对此缓存机制加以整理,以备后用。
我的图片缓存策略
具体来说,图片缓存一般包括两部分:URL缓存和存储缓存,或曰“内存缓存”和“本地缓存”。前者是将图片存储到内存中,后者是将图片存储到本地磁盘中。因此,当UIImageView控件需要加载一张图片,图片来源可以由三个地方:内存、本地磁盘、网络服务器;消耗成本排序也是:内存 < 本地磁盘 < 网络服务器。
UIImageView(AFNetworking)完成得并不全面,它只做了“URL缓存”,并没有做“存储缓存”,此外,它对“URL缓存”处理比较粗糙。举个栗子,现在App需要下载URL为http://example.com/test.png
的图片并“装”到UIImageView控件中,UIImageView(AFNetworking)的处理策略是:
- 通过网络下载图片;
- 将下载图片存到内存中,存储形式为一个键值对,key为URL,value为下载图片;
- 当再次需要加载URL为
http://example.com/test.png
的图片时,就到内存中把这个图片给找出来,直接“装”到UImageview控件中,而无需通过网络重新下载。
但是在很多场合,在不同的时间同一个URL所对应的图片是不一样的,譬如用户头像,假设其user id为123456,则其头像URL通常为http://example.com/123456/123456.png
,用户一般可以自定义头像,所以这个地址的图片常会发生变化…笔者所经历的项目的做法是:当客户端向服务端所要图片信息时,服务器返回的图片信息至少包括两个部分:图片URL和图片的hash值(在服务器里,每个图片都有自己的hash值)。如此这般,客户端在检测图片是否在内存中时不仅仅需要检查URL,还得检查hash值。
总之,针对笔者的项目,UIImageView(AFNetworking)存在两个不足:
- “URL缓存”过于粗糙;
- 没有“存储缓存”;
笔者针对这两个不足对UIImageView(AFNetworking)进行了加工,最后的缓存策略是:
- 根据图片URL和哈希值查找cache是否有这张图片。如果没有,则进入第2步;如果有,则进一步判断图片的hash值和cache中的哈希值是否一样,如果不一样,则进入第2步;
- 根据图片哈希值查找物理存储(本地磁盘)是否有这张图片,有则返回图片,没有则进入第3步;
- 从网络上下载该图片,下载完后保存到内存和本地磁盘中,前者使用NSCache进行存储,存储信息包括:图片URL、图片哈希值、图片;存储到本地磁盘的图片命名为“图片的哈希值.png”。
SDWebImage的图片缓存策略
SDWebImage是一个很厉害的图片缓存的框架。AFNetworking集成的UIImageView+AFNetworking.h对图片的缓存实际应用的是NSURLCache自带的cache机制。而NSURLCache每次都要把缓存的raw data再转化为UIImage,就带来了数据处理和内存方面的更多操作。具体的比较在这里。
SDWebImage提供了如下三个category来进行缓存:
- MKAnnotationView(WebCache)
- UIButton(WebCache)
- UIImageView(WebCache)
以最为常用的UIImageView(WebCache)为例:
- setImageWithURL:placeholderImage:options:先显示placeholderImage,同时由SDWebImageManager根据URL来在本地查找图片;
- SDWebImageManager:downloadWithURL:delegate:options:userInfo:SDWebImageManager是将UIImageView+WebCache同SDImageCache链接起来的类,SDImageCache:queryDiskCacheForKey:delegate:userInfo:用来从缓存根据CacheKey查找图片是否已经在缓存中;
- 如果内存中已经有图片缓存,SDWebImageManager会回调SDImageCacheDelegate:imageCache:didFindImage:forKey:userInfo:;
- 而UIImageView+WebCache 则回调SDWebImageManagerDelegate:webImageManager:didFinishWithImage:来显示图片;
- 如果内存中没有图片缓存,那么生成NSInvocationOperation添加到队列,从硬盘查找图片是否已被下载缓存;
- 根据URLKey在硬盘缓存目录下尝试读取图片文件。这一步是在NSOperation进行的操作,所以回主线程进行结果回调notifyDelegate:;
- 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:进而回调展示图片;
- 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:;
- 共享或重新生成一个下载器SDWebImageDownloader开始下载图片;
- 图片下载由NSURLConnection来做,实现相关delegate来判断图片下载中、下载完成和下载失败;
- connection:didReceiveData:中利用ImageIO做了按图片下载进度加载效果;
- connectionDidFinishLoading:数据下载完成后交给SDWebImageDecoder做图片解码处理;
- 图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI,如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多;
- 在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:回调给SDWebImageDownloader;
- imageDownloader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成;
- 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片;
- 将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存;
- 写文件到硬盘在单独NSInvocationOperation中完成,避免拖慢主线程;
- 如果是在iOS上运行,SDImageCache在初始化的时候会注册notification到UIApplicationDidReceiveMemoryWarningNotification以及UIApplicationWillTerminateNotification,在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片;
- SDWebImagePrefetcher可以预先下载图片,方便后续使用;
毫无疑问,SDWebImage比UIImageView(AFNetworking)要强大得多,但也因此变得更加复杂了,使用起来麻烦也多了很多;就我认为,对于比较简单的场合,考虑到使用的方便性,UIImageView(AFNetworking)会是一个更好的选择。