一个好的图片加载缓存框架是怎么样炼成的?

SDWebImage基本上是app开发的标配了,它的源码我一直没有去看。因为我觉得图片加载缓存很容易,无非是先看两级缓存有没,有就加载,没有就去服务端获取并缓存下来。无非在图片的解码,缓存的策略上显点神通提高效率罢了。然而我还是太年轻了,当自己真正地去实现一个图片加载缓存的功能时,就发现里面的学问还是很大的。

几个问题

首先考虑最常见的情况,就是tableView上每个cell都有一个头像,然后只像SD那样sd_setImageUrl。基于此先问4个小问题,如果4个问题都有答案了,那就不用在往下看了。

  1. tableView有cell复用问题,用户下拉后,原先的图片请求完成,cell会加载导致头像乱序,怎么避免?
  2. 有个请求失败了,接下去有相同的请求怎么办?
  3. 用户快速下拉,同时请求好多图片,怎么控制线程数?
  4. 用户滑动的过程中,有相同的Url,怎么避免重复请求,并且每个cell的回调都能执行?

SDWebImage的答案

问题1

最简单的方法:把图片的url放入cell中,异步加载完成的时候判断其url是否与cell内的相同,相同的时候再setImage。
当然这一点都不优雅,而sd_setImageUrl里,把每次请求当成operation,每次都会先cancel operation,在把operation存起来。这样保证了旧的请求被cancel了,非常优雅。

问题2

@property (strong, nonatomic) NSMutableSet<NSURL *> *failedURLs;

...
BOOL isFailedUrl = NO;
if (url) {
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
}

if (isFailedUrl) {
   return;
}
...

...
if (error.code != NSURLErrorNotConnectedToInternet
    && error.code != NSURLErrorCancelled
    && error.code != NSURLErrorTimedOut
    && error.code != NSURLErrorInternationalRoamingOff
    && error.code != NSURLErrorDataNotAllowed
    && error.code != NSURLErrorCannotFindHost
    && error.code != NSURLErrorCannotConnectToHost) {
    @synchronized (self.failedURLs) {
        [self.failedURLs addObject:url];
    }
}

...

类似上面的处理,只要有请求失败的就会加入到failedURLs里,下次就不用再请求了。

问题3

控制线程数可以用NSOperationQueue,它有个maxConcurrentOperationCount参数可以设置最多能并发运行的operation个数。虽然operation数并不等于线程数,但是确实可以控制线程个数。

问题4

//WebImageDownloader
@property (nonatomic, strong) NSMutableDictionary<NSURL *, WebImageDownloaderOperation *> *urlOperations;

//WebImageDownloaderOperation.h
@property (nonatomic, strong) NSMutableArray<TRWebImageDownloadComplete> *callBackBlocks;

类似上面这两个属性,每个url只对应一个operation,这样每个url只会请求一次。
每个operation里有个callBack数组,每当operation完成就遍历,这样所有callBack都能执行。

结尾

当然SDWebImage不仅仅解决了上面四个问题,他自己也实现了图片缓存,各种图片格式的解码等等,不过这些都能找到更好的替代品。
我还是觉得SDWebImage最值得学习的是它的结构。耦合性非常好,而对外仅仅暴露了一个最最简单的接口,这真的值得学习。

作者:levi
comments powered by Disqus