SDWebImage是一个提供UIImageView和UIButton类异步加载图片并且缓存的框架,接口简洁,类的分工十分明确。框架文件也不少,但主要围绕下载缓存的实现去解读就容易理清楚。
我们以UIImageView的下载为例来探究。
UIImageView+WebCache
当我们使用图片异步加载时,是用分类UIImageView+WebCache中的接口。该分类提供了多种设置图片的方式,但是最终都会调用如下方法(这也是一个常见的设计思想)。
//设置图片
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
当然,可以设置单张图片,也可以设置gif图片(多张图片)。也可以取消对应图片的下载。
//设置gif图片(多张图片)
- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;
//取消图片下载
- (void)sd_cancelCurrentImageLoad;
//取消gif图片下载(多张图片)
- (void)sd_cancelCurrentAnimationImagesLoad;
除了设置图片外,UIImageView+WebCache还提供设置菊花指示器的功能(UIActivityIndicatorView),以便在加载时可以控件中看到旋转的菊花指示器,起到提示用户的功能。
//设置是否显示
- (void)setShowActivityIndicatorView:(BOOL)show;
//设置样式
- (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style;
了解完UIImageView+WebCache的功能,我们主要来看看怎么设置单张图片的?
//设置单张图片的主要调用入口
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
//取消当前的下载???
[self sd_cancelCurrentImageLoad];
//关联对象,相当于有个url保存在self中
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果不是下载好再加载占位符的模式 就在主线程设置占位符
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// 添加菊花
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
//移除菊花
[wself removeActivityIndicator];
//如果控制器被释放,就不用继续执行
if (!wself) return;
dispatch_main_sync_safe(^{
//如果控制器被释放,就不用继续执行
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)//有图片,非自动设置图片的模式,传出结果就好
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {//有图片,自动设置图片
wself.image = image;
[wself setNeedsLayout];//必要吗?
} else {//没图片
//下载好再加载占位符的模式,设置占位图片(其他模式肯定已经设置好了)
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
//UIView的operations字典保存UIImageViewImageLoad和UIImageViewAnimationImages的operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
//提示错误
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
当给UIImageView设置单张图片时,会进行如下操作:
① 取消当前的下载操作。
② 通过动态关联,将imageURL保存起来,成为UIImageView的属性。
③ 根据传入的模式来设置占位符。
④ 通过判断url来决定返回错误或者通过url去获取,获取的话就是交给SDWebImageManager了。
下载时,会将对应的下载操作保存起来,在UIView+WebCacheOperation中,动态关联了一个operations的字典,用来保存一个下载单张图操作,和一个下载图片组操作。之所以把保存operation的功能放在UIView的分类中,也是为了能让其他UIView的子类,比如UIButton使用。
那为什么要保存对应的操作呢?我想作者是把UIImageView分成可以同时设置单张图片和图片组两种方式,这两张设置不冲突,但是你设置单张图片的时候,就必须把你上次设置单张图片的操作取消。
//动态关联对象,解决分类创建不了属性的问题,提供operations属性的功能
//这边有点类似于懒加载(先获取,如果没有再创建)
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
前面讲完了怎么设置图片的方法,最主要的通过url获取图片的方法(下载或者缓存)还是在SDWebImageManager中。
SDWebImageManager
SDWebImageManager是一个管理类,里面有这么两个重要的属性(缓存和下载器)。
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
SDWebImageManager是一个单例对象,在创建时也自动创建了缓存和下载器两个对象。
+ (id)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (instancetype)init {
SDImageCache *cache = [SDImageCache sharedImageCache];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
return [self initWithCache:cache downloader:downloader];
}
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader {
if ((self = [super init])) {
_imageCache = cache;
_imageDownloader = downloader;
_failedURLs = [NSMutableSet new];
_runningOperations = [NSMutableArray new];
}
return self;
}
关于缓存和下载器具体的功能和设计可以看我的这两篇文章。
【源码解读】SDWebImage ─── 缓存的设计
【源码解读】SDWebImage ─── 下载器的设计
SDWebImageManager提供的功能其实就是缓存和下载器的组合:你提供一个url,我去缓存(imageCache)中(先内存缓存再磁盘缓存)寻找,如果没找到就让下载器(imageDownloader)去下载。
当我们通过SDWebImageManager获取图片时,调用的是:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
我们不仅需要传入url,下载中的回调progressBlock,完成后的回调completedBlock,还要传入options,这个options可以是单个也可以是多个,主要用来控制图片的设置过程(包括缓存和下载),比如传入SDWebImageAvoidAutoSetImage就需要我们手动去将下载完成后的图片赋值给UIImageView。可以说options是为了满足一些特殊的需求,当options为nil时就是一些通用的过程。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0,//失败后重试
SDWebImageLowPriority = 1 << 1,//低优先级,比如UIScrollow滚动时不下载
SDWebImageCacheMemoryOnly = 1 << 2,//只进行内存缓存
SDWebImageProgressiveDownload = 1 << 3,//渐进式下载,图片渐进式显示
SDWebImageRefreshCached = 1 << 4,//刷新缓存
SDWebImageContinueInBackground = 1 << 5,//后台下载
SDWebImageHandleCookies = 1 << 6,//存储Cookies
SDWebImageAllowInvalidSSLCertificates = 1 << 7,//允许非法证书
SDWebImageHighPriority = 1 << 8,//放在高优先级的队列
SDWebImageDelayPlaceholder = 1 << 9,//延迟占位图片出现的时间,等下载完成
SDWebImageTransformAnimatedImage = 1 << 10,//gif的动画????
SDWebImageAvoidAutoSetImage = 1 << 11//下载完不自动设置图片
};
接下来我们来看SDWebImageManager具体是怎么设计的?
当我们传入url,下载中的回调progressBlock,完成后的回调completedBlock,以及相关的options来SDWebImageManager获取图片时,会进行如下操作(后面附上源码和解析)。
① 判断不可以下载的情况(url不可用,该url曾经下载失败过)
这边也可以看出如果不是设置成SDWebImageRetryFailed,一旦url下载失败过,就会加入failedURLs,下次下载该url时,直接返回错误信息。
② 到这一步证明url可以下载。创建一个SDWebImageCombinedOperation对象,并添加进runningOperations中。
SDWebImageCombinedOperation是一个组合操作(里面有cacheOperation的属性),后面会将imageCache查询获取的操作赋值给cacheOperation。
runningOperations是为了取消操作,判断是否有正在进行的操作。
③ 将imageCache查询获取的操作赋值给组合操作operation中的cacheOperation,并将该operation返回给UIImageView的分类。
在imageCache查询结果的回调中,主要分成三种情况:
如果缓存中没有图片
通过options去设置imageDownloader的downloaderOptions,然后通过imageDownloader创建一个下载队列subOperation。
imageDownloader下载完成的回调又有以下三种情况:
1)被取消,不需要做什么
2)有错误,返回对应错误,并添加进failedURLs
3)成功下载,没有下载图片的情况就是NSURLCache有缓存,有下载图片的情况,如果有实现转换图片的代理,就先转换,再通过imageCache储存起来,并且在主线程回调completedBlock。如果缓存中有图片
在主线程回调completedBlock,将image传出去。将operation从runningOperations移除。如果缓存没有图片,下载操作也不被允许(通过代理来设置)
在主线程回调completedBlock,将nil和SDImageCacheTypeNone传出去。将operation从runningOperations移除。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
/* 容错机制 */
//completedBlock不能为空
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//如果误传NSString类型,就转成NSURL类型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 如果传的是其他乱七八糟的类型,比如NSNull,就置为nil
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
//创建一个混合操作(其实是缓存操作)
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
//加锁,获取不可用集合里是否有包含该url
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//如果url为空,或者(options为不是重新下载错误url且是错误url)
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
//在主线程回调错误信息,并返回
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
//到这步,证明可以下载
//加锁,添加到正在下载的集合runningOperations中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//通过url获取key(没设置过滤器的情况就是装成NSString)
//过滤器的作用就是让用户来自定义装换的标准
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
//如果操作被取消,就从当前运行的操作数组中剔除
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
#pragma mark - 如果没有图片 || 需要更新缓存的类型 && 下载代理能响应方法
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//如果有图片,但是是需要更新缓存的类型
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES, url);
});
}
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;//低优先级
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;//渐进式下载
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;//使用NSURLCache
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;//后台下载
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;//使用cookies
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;//允许非法SSL
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;//高优先级
//如果有图片,但是是需要更新缓存的类型
if (image && options & SDWebImageRefreshCached) {
// 因为有图片了,只是更新缓存,所以关掉渐进式下载(减去该枚举)
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 忽略 NSURLCache的响应(加上该枚举)
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
/* ===============下载的Block============== */
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//1.被取消
if (!strongOperation || strongOperation.isCancelled) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
}
else if (error) {//2.有错误
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if ( error.code != NSURLErrorNotConnectedToInternet//无连接
&& error.code != NSURLErrorCancelled//连接取消
&& error.code != NSURLErrorTimedOut//超出时间
&& error.code != NSURLErrorInternationalRoamingOff//网络中断
&& error.code != NSURLErrorDataNotAllowed//不允许数据
&& error.code != NSURLErrorCannotFindHost//不能发现主地址
&& error.code != NSURLErrorCannotConnectToHost) {//不能连接主地址
//如果不是以上特殊情况,就判定该url不可用
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {//3.正常下载完成
//如果是重试错误的url,就从失败的url数组中剔除该url
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
//是否有缓存在磁盘
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// 重新下载选项 && 缓存有图片 && 没有下载图片
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
//如果有下载图片&&(不是gif)&&有响应图片转换的代理
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//获取变形(转换)后的图片
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
//如果变形后图片有值且下载完成
if (transformedImage && finished) {
//是否有变形
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
//将图片储存起来,根据是否转换来决定是否重新计算size
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
//操作没取消的话就在主线程回调completedBlock
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
else {//有下载图片
//缓存图片
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
//操作没取消的话就在主线程回调completedBlock
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
//如果下载完成,把下载操作从下载操作数组中剔除
if (finished) {
@synchronized (self.runningOperations) {
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
}
}];
/* ===============取消的Block============== */
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
}
#pragma mark - 如果缓存中有图片
else if (image) {
//直接在主线程回调
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
#pragma mark - 缓存没有图片,下载操作也不允许
// Image not in cache and download disallowed by delegate
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}