前言:
SDWebImage应该是我们iOS开发最常用的第三方框架之一,通过以分类的方式,为我们提供网络图片的加载、缓存等操作,接下来就一边阅读源码,一边记录一下实现过程。
首先需要提前知道的一些知识点:
SDWebImageOptions
SDWebImageOptions是对外暴露的一些option,我们通过使用option来达到一定的目的效果,读源码时会发现有很多地方根据option有不同的操作,所以理解option还是挺重要的。在内部还有一些option是不能给调用者直接使用的,比如SDImageCacheOptions, SDWebImageDownloaderOptions,其实这两个option都会根据SDWebImageOptions的设定来确定。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
//图片下载失败时会被加入黑名单,默认不会再次下载,设置这个就不会加入黑名单
SDWebImageRetryFailed = 1 << 0,
//默认情况下,图片下载在UI交互时也会进行,设置这个会延迟下载时机
SDWebImageLowPriority = 1 << 1,
//在下载完成后禁用磁盘缓存,仅在内存中缓存
SDWebImageCacheMemoryOnly = 1 << 2,
//默认下图片下载完后显示,这个设置下载过程中图片可以一点点显示,渐进式,类似于网页
SDWebImageProgressiveDownload = 1 << 3,
//刷新缓存,会回调两次complete,第一次原缓存图片,第二次新图片
SDWebImageRefreshCached = 1 << 4,
//后台继续下载
SDWebImageContinueInBackground = 1 << 5,
//通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES;存储cookies
SDWebImageHandleCookies = 1 << 6,
//允许使用未受信任的SSL证书
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
//默认图片下载顺序是根据加入队列的顺序执行的,这个选项可以改变优先级
SDWebImageHighPriority = 1 << 8,
//默认直接显示占位图,这个选项可以延迟这个时机,直到图片加载完成
SDWebImageDelayPlaceholder = 1 << 9,
//我们通常不会在动画图像上调用transformDownloadedImage委托方法,
//因为大多数转换代码都会出错
//这个选项让我们无论如何都来转换它们。(翻译。。)
SDWebImageTransformAnimatedImage = 1 << 10,
//默认图片加载完成自动显示,这个选项可以让我们手动显示图片
SDWebImageAvoidAutoSetImage = 1 << 11,
//如果图片太大,可以通过这个设置让图片压缩到一个合适的大小,
//如果设置了“SDWebImageProgressiveDownload”标志,则将关闭缩放功能。
SDWebImageScaleDownLargeImages = 1 << 12,
//默认内存缓存中找到就返回,这个设置强制在内存中找到图片时,再继续向磁盘中查询
SDWebImageQueryDataWhenInMemory = 1 << 13,
//默认情况下,我们同步查询内存缓存,异步查询磁盘缓存。
//这个选项可以强制同步查询磁盘缓存,以确保在相同的运行循环中加载映像。
//如果禁用内存缓存或在其他情况下,此标志可以避免在单元格重用期间闪烁。
SDWebImageQueryDiskSync = 1 << 14,
//只查询缓存,没有也不进行下载
SDWebImageFromCacheOnly = 1 << 15,
//默认情况下,SDWebImageTransition只对网络图片进行处理,
//这个选项可以对缓存图片也进行transition
SDWebImageForceTransition = 1 << 16
}
代码中有不少地方判断这些option,所以提前记一下每个选项是什么意思,看到源码的时候也就知道为什么是这样一个逻辑了。
接下来我们再看一下SDWebImage图片加载的时序图
1.SDWebImage为UIImageView、UIButton等都创建了一个分类,也就是为我们提供使用入口方法
[imageView sd_setImageWithURL:[NSURL URLWithString:@""]];
2.这个方法最终都会走到(UIImageView+webCache)的这个方法中,里面又调用(UIView+webCache)中的方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
//正常使用sd_setImageWithURL:加载图片时,最终都会走到这个方法,该方法是UIView (WebCache)类下的方法
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
3.UIView (WebCache)的上述方法中会创建一个SDWebImageManager的实例对象,该对象调用其下列方法来加载图片:
-(id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
4.在加载图这个Operation中进行一系列的查询缓存、磁盘的操作,通过回调在获取到图片后,SDWebImageManager经过一步步的回调,将图片返回给调用者。
接下来进行源码分析,我是读代码加参考别人的分析,直接在代码里写了注释,就直接放上来了。
1.先看入口方法
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context {
//根据validOperationKey,先取消掉之前加载图片的operation,如果key是nil,key通过类名生成
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//动态添加属性,添加一个名字为imageURLKey的属性, 值为url。可以获取这个控件上图片的url
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果没有延迟显示展位图,先加载占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
#endif
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
__weak __typeof(self)wself = self;
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
//通过SDWebImageManager 加载图片 调用loadImageWithURL方法去加载图片,返回值是SDWebImageCombinedOperation
//这个SDWebImageCombinedOperation对象是一个<SDWebImageOperation>代理对象
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];
#endif
// if the progress not been updated, mark it to complete state
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
//是否不显示图片 (图片存在&&调用者手动主动配置)|| (没有图片&&不delaye占位图情况)
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
//如果设置了不自动显示图片,则回调让调用者手动添加显示图片
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
//调用上面的block: callCompletedBlockClojure
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
//如果没有image,并且调用者设置了delaye显示默认图那这里targetImage设置为placeholder
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
}];
//添加operation,这里使用的是NSMapTable,对operation是弱引用
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
总结:
1.首先判断当前view是否已经有operation,如果有就将此操作取消并移除
2.添加imageURLKey属性,可以通过这个获取空间上当前图片的url,占位图等操作
3.创建SDWebImageManager对象 ,DownloaderProgressBlock,并通过manager创建图片下载的operation,最后把operation存在了NSMapTable中
- operation下载回调中有对图片的处理
2. SDWebImageManager对象的创建operation方法,loadImageWithURL:options:progress:completed:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 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.
//如果url是string 转为URL
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;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
//self.failedURLs是nsurl的黑名单 这里是看url是否在黑名单内
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果url长度是0 || (options没有选择重新加载FailedUrl && url在黑名单内),直接返回
//SDWebImageRetryFailed是即时url在黑名单内也加载
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
//将operation添加到runningOperations内
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];
//这里是把options(SDWebImageOptions,对于我们使用来说,SD只暴露了SDWebImageOptions)转换为对应的SDImageCacheOptions
//options属于位移枚举,不懂可以搜索一下,比较好理解
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
//这里先使用SDImageCache对象,通过queryCacheOperationForKey: 获取key(根据图片url生产的key)对应的图片
__weak SDWebImageCombinedOperation *weakOperation = operation;
//查询缓存,使用了
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// Check whether we should download image from network
// shouldDownload =(不是只从cache加载图片)&&(图片不存在||需要刷新缓存)&&(没有实现||调用方法,没实现会返回yes)
//imageManager:shouldDownloadImageForURL: 这个方法是阻止缓存丢失时下载图像。如果没有实现,则表示YES。 这里结合表示允许下载
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
//如果允许下载
if (shouldDownload) {
//如果 (缓存图片存在 && options是刷新缓存),那么会回调2次,当前这次先返回缓存图片,后续进行加载、刷新操作
if (cachedImage && options & SDWebImageRefreshCached) {
// 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.
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
//这里是把options(SDWebImageOptions)转换为对应的SDWebImageDownloaderOptions
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 (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
//如果 (缓存图片存在 && options设置需要刷新缓存)
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
//这里其实就是把SDWebImageDownloaderProgressiveDownload这个option去掉
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
//加上SDWebImageDownloaderIgnoreCachedResponse这个option,忽略NSURLCache中的缓存
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
__weak typeof(strongOperation) weakSubOperation = strongOperation;
//通过SDWebImageDownloader 对象下载图片
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
//如果 strongSubOperation=nil 或者 已经cancel
if (!strongSubOperation || strongSubOperation.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) {
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// Check whether we should block failed url
//是否把加载失败的时候 标记失败的url,加入failedURLs,url黑名单
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else {
//如果设置了SDWebImageRetryFailed那么就要把URL从黑名单中移除
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
//是否在硬盘上缓存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
//没有downloadedImage,不做处理
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
//这段是transformedImage,后续再分析,到后面的else中
} 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];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//这段不懂,没有实现self.cacheSerializer。好像是并不会走这里
//这里应该是通过self.cacheSerializer转换出新的图片的data
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
});
} else {
//直接走这里,缓存图片
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//这里会通过callCompletionBlockForOperation: 第二次回调completedBlock,加载新的图片。
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
} else if (cachedImage) {
//这里是接着上面的if (shouldDownload),不允许下载,缓存图片存在,那么直接返回
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
//这里即 不允许下载,并且缓存图片也不存在。
// Image not in cache and download disallowed by delegate
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
return operation;
}
总结:
loadImageWithURL:
1.首先判断url有效性,根据option决定后续操作
2.根据url查询缓存,回调中再根据option决定是否需要下载。
如果不需要下载,根据缓存图片情况callback;如果需要下载,开始下载,下载回调中,缓存图片到内存和磁盘,然后callback。
3.然后看一下查询缓存的过程,在NSCache中queryCacheOperationForKey:方法
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
//key是空就直接返回
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
//先从内存缓存中查找,key是图片的url
UIImage *image = [self imageFromMemoryCacheForKey:key];
//SDImageCacheQueryDataWhenInMemory,如果图片存在 && 没有设置(这里用的!),那么只查询内存缓存,并返回。否则后面继续查询硬盘缓存
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
//从磁盘查询,为什么用@autoreleasepool还不懂,后续在看,先看查询操作
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
//缓存的类型,有三种类型,none,memory,disk
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
// the image is from in-memory cache
//这里是内存缓存
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// decode image data only if in-memory cache missed
//这里是从磁盘缓存中取图片
diskImage = [self diskImageForKey:key data:diskData options:options];
//判断是否要缓存到内存缓存,默认是yes
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
//这里是根据下面代码选择是 同步还是异步 切换线程调用doneBlock
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
//SDImageCacheQueryDiskSync 是要求同步查询磁盘缓存
//默认是同步查询内存缓存, 异步查询磁盘缓存
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
总结:
首先查询内存缓存,根据option决定是否查询磁盘缓存。在查询磁盘缓存的回调中,如果磁盘中查到缓存,内存中没有查到,那么就缓存到内存缓存中(默认,实际是不是需要根据option决定)。
4.再看图片的下载操作,SDWebImageDownloader对象的downloadImageWithURL:方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
//return了下面这个addProgressCallback:方法,这里有createCallback,这个bolck的实现,里面创建了下载图片的Operation。我们先看addProgressCallback:这个方法
(ps:之前版本是调用一个方法创建一个下载的operation,这里改为了callback,这里只是有operation的实现,具体对这个operation的操作在addProgressCallback:的方法中)
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
//创建一个request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
//使用request和session对象去创建下载的operation
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
//设置operation的优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//添加依赖 这里是后进先出
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
紧接着就是addProgressCallback:方法
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
if (!operation || operation.isFinished) {
//创建一下下载的operation,通过传递过来的createCallback参数,前一个方法已经有了这个block的实现,返回一个SDWebImageDownloaderOperation对象
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
//把operation加入到NSOperationQueue中
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//一个取消operation的token
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
总结:
这里SDWebImageDownloader主要就是创建下载图片的operation,并且加入到downloadQueue(NSOperationQueue对象)中,通过这个downloadQueue来管理operation。
createCallback:中主要就是创建request,然后通过request和session创建下载的operation。
5.再看一下SDWebImageDownloaderOperation对象的创建方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
if ((self = [super init])) {
_request = [request copy];
_shouldDecompressImages = YES;
_options = options;
_callbackBlocks = [NSMutableArray new];
_executing = NO;
_finished = NO;
_expectedSize = 0;
_unownedSession = session;
_callbacksLock = dispatch_semaphore_create(1);
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
总结:
这里就是一些初始化工作。
6.把operation加入到队列后,会自动调用operation 的start方法,我们再看一下。
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if SD_UIKIT
//后台下载
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
//创建session
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
//真正下载任务的task
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
//开始下载
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
总结,这里就是通过上面初始化operation时保存下来的一些初始化数据,创建真正下载图片所需要的task,然后resume开启任务。
到这里,主要流程就分析完了,还有一些辅助模块,比如图片的解码这些没有仔细去看。
收获:对于这些优秀的开源框架,个人感觉最突出的就是分层明确,职责清晰,还有一些巧妙的小用法。比如SDMemoryCache中,除了本身继承NSCache,这里还有个weakCache属性,用的是NSMapTable(类似于字典,具体差别可度娘)存储,在存储时除了使用系统的NSCache还存到了weakCache中。这里是以空间换时间的一种方式。
个人阅读时主要参考SDWebImage源码解读
有些注释类似,但是也是已经理解过了。欢迎小伙伴们一起讨论,共同进步!