我们在上一篇文章介绍了 SDWebImage 框架的 UIImageView+WebCache 扩展类,主要功能就是设计了严密的判断,保证运用该框架的其他开发者能够在各种环境下获取图片。这篇文章主要是介绍 SDWebImageManager 中从缓存获取图片或者从网络下载图片。
第一部分:获取图片对应的 key
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
// 调用这个方法必须实现 completedblock
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 下面两段代码都是为了保护 url 逻辑性
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 创建一个 operation
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 判断当前的url,是否包含在下载失败的url集合中,在访问failedURLs集合时要加锁,防止其他线程对其进行操作
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
// 如果url为空 || 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;
}
// 将 operation 加到运行中的url集合中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 从缓存中根据url取出key(其实key就是url的string)
NSString *key = [self cacheKeyForURL:url];
// 根据key去查找对应的图片 ???
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
... // 详情见 第二部分
}];
return operation;
}
第二部分:根据 image 判断是否需要从网络获取
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
...
}];
上面的代码调用了 SDImageCache 中的 queryDiskCacheForKey: done: 方法,我们后续章节再研究这个类中的东西,我们先看看对这个方法中的 block 的处理,也就是结果的处理方法:
// 判断该任务是否已经cancel了
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
// 先看 && 的后半段
// 注意后面的 A || B,如果 A 为真,那就不用判断 B 了
// 也就是说,如果 imageManager:shouldDownloadImageForURL: 方法没有实现,直接返回 YES
// 目前我在源码中并没有看到函数的实现,所以就当 if 的后半段恒为 YES
// 我们主要看 && 前面的 || 表达式
// 如果缓存中没找到 image,或者需要刷新内存,就执行 if 语句
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
... // 详情见第三部分
}
// 从缓存中找到 image
else if (image) {
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
// 将该 operation 从数组中移除
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
// Image not in cache and download disallowed by delegate
// 又没有从缓存中获取到图片,shouldDownloadImageForURL 又返回 NO,不允许下载,悲催!
// 所以 image 和 error 均传入 nil
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];
}
}
第三部分:初始化下载图片
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
...
}
上面判断 if 中实现的内容就是 image 需要网络获取部分,详情如下:
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.
// 如果图片在缓存中找到,但是options中有SDWebImageRefreshCached
// 那么就尝试重新下载该图片,这样是NSURLCache有机会从服务器端刷新自身缓存
completedBlock(image, nil, cacheType, YES, url);
});
}
// download if no image or requested to refresh anyway, and download allowed by delegate
// 首先定义枚举值 downloaderOptions,并根据 options 来设置 downloaderOptions
// 基本上 options 和 downloaderOptions 是一一对应的,只需要注意最后一个选项 SDWebImageRefreshCached
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (image && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
// 相当于downloaderOptions = downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
// ~ 位运算,取反
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
// 相当于 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
// 因为SDWebImage有两种缓存方式,一个是SDImageCache,一个就是NSURLCache
// 所以已经从SDImageCache获取了image,就忽略NSURLCache了。
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 下载图片 (图片下载部分将在后续章节解读,这里先不介绍)
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
... // 详情见 第四部分
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
第四部分:下载图片完成后 block 中的回调
图片下载部分将在后续章节讲解,这里先介绍一下图片下载完成后 block 中的内容:
__strong __typeof(weakOperation) strongOperation = weakOperation;
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) {
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) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
// 如果是失败重试,那么将 url 从 failedURLs 中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
// 是否只能从内存中取出
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// 如果需要刷新缓存,并且存在 image,并且没有新下载的 image
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
// 如果有新下载的 image,并且需要对 image 进行处理,并且实现了图片处理的代理方法
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];
// 将图片缓存到内存中(这里后续讲解)
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
// 将图片传出
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];
}
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];
}
}
}
总结
SDWebImageManager 类中主要是对图片下载和缓存方式的管理,以及对下载完成的图片,url 等信息的回调,承上启下的作用,承接了 UIImageView+WebCache 类,下接了 SDImageCache 类和 SDWebImageDownloader 类。
SDWebImage 源码解析 github 地址:https://github.com/Mayan29/SDWebImageAnalysis
遗留问题:
根据 key 去缓存中查找对应的图片
SDImageCache 类
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
下载图片
SDWebImageDownloader 类
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
将图片存入缓存
SDImageCache 类
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk