【iOS】SDWebImage第三方库源码学习笔记
前言
之前在写项目时,经常用到SDWebImage这个第三方库来加载图片,并且了解到了这个第三方库在处理图片时自带异步下载和缓存功能,以及对cell复用的处理。这篇文章来系统学习一下SDWebImage第三方库的知识以及底层原理
简介
SDWebImage
为UIImageView
、UIButton
提供了下载分类,可以很简单地实现图片异步下载与缓存功能。SDWebImage
的第三方库具有以下特性:
异步下载图片
异步缓存(内存+磁盘),自动管理缓存有效性
同一个URL不会重复下载
自动识别无效URL,不会反复重试
不阻塞主线程
使用GCD与ARC
用法
1.在UITableView中使用UIImageView+WebCache
UITabelViewCell
中的 UIImageView
控件直接调用 sd_setImageWithURL: placeholderImage:
方法即可
2.使用回调Blocks
[listTableViewCell.sightsImageView sd_setImageWithURL:(nullable NSURL *) completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {NSLog(@"回调");}];
3.使用SDWebImageManager单例类
SDWebImage是一个单例类,也是SDWebImage库中的核心类,负责下载与缓存的处理
+ (nonnull instancetype)sharedManager {static dispatch_once_t once;static id instance;dispatch_once(&once, ^{instance = [self new];});return instance;
}
- (nonnull instancetype)init {id<SDImageCache> cache = [[self class] defaultImageCache];if (!cache) {cache = [SDImageCache sharedImageCache];}id<SDImageLoader> loader = [[self class] defaultImageLoader];if (!loader) {loader = [SDWebImageDownloader sharedDownloader];}return [self initWithCache:cache loader:loader];
}
可以看到SDWebImageManager将图片下载和图片缓存组合起来了,用法如下:
SDWebImageManager *manager = [SDWebImageManager sharedManager];[manager loadImageWithURL:(nullable NSURL *) options:(SDWebImageOptions) progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {}];
4.单独使用SDWebImageDownloader异步下载图片
使用SDWebImageDownloader可以异步下载图片,但是图片不会缓存到磁盘或内存
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];[downloader downloadImageWithURL:(nullable NSURL *) options:(SDWebImageDownloaderOptions) progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {}];
5.单独使用SDImageCache异步缓存图片
SDImageCache可以和SDWebImageDownloader一样使用单例来缓存数据,支持内存缓存和异步的磁盘缓存
添加缓存:
[[SDImageCache sharedImageCache] storeImage:(nullable UIImage *) forKey:(nullable NSString *) completion:^{}];
默认情况下,图片数据会同时缓存到内存和磁盘中,如果只想要内存缓存的话,可以使用下面的方法:
[[SDImageCache sharedImageCache] storeImage:image forKey:(nullable NSString *) toDisk:NO completion:^{}];
或者:
[[SDImageCache sharedImageCache] storeImageToMemory:(nullable UIImage *) forKey:(nullable NSString *)];
读取缓存可以使用以下方法:
[[SDImageCache sharedImageCache] queryCacheOperationForKey:(nullable NSString *) done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {}];
实现原理
sd_setImageWithURL
我们查看sd_setImageWithURL方法是如何实现的可以发现,这个方法在UIImageView+WebCache文件中,并且这个文件中所有与这个方法类似的方法最后都会调用下面这个方法:
因此为Cell的UIImageView加载图片的原理就藏在这个方法中,来看这个方法是怎么实现的:
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)urlplaceholderImage:(nullable UIImage *)placeholderoptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextsetImageBlock:(nullable SDSetImageBlock)setImageBlockprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// 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 is NSString and shouldUseWeakMemoryCache is true, [cacheKeyForURL:context] will crash. just for a global protect.if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];//SDWeb允许传入NSString类型}// Prevents app crashing on argument type error like sending NSNull instead of NSURLif (![url isKindOfClass:NSURL.class]) {url = nil;//防止不是URL类型导致崩溃}if (context) {// copy to avoid mutable objectcontext = [context copy];//创建副本以避免直接修改可变对象} else {context = [NSDictionary dictionary];//如果没有提供上下文则创建一个空的字典作为上下文}NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];//尝试从上下文中获取键值if (!validOperationKey) {// pass through the operation key to downstream, which can used for tracing operation or image view classvalidOperationKey = NSStringFromClass([self class]);SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;context = [mutableContext copy];}//valid无效就以当前类名作为操作键创建一个self.sd_latestOperationKey = validOperationKey;//更新最新操作键if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) {// cancel previous loading for the same set-image operation key by default[self sd_cancelImageLoadOperationWithKey:validOperationKey];}//默认情况下,如果没有设置SDWebImageAvoidAutoCancelImage选项,则取消与当前设置图片操作键相关联的所有先前的下载操作。 可以避免复用导致的问题SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey];//获取或创建与当前操作键关联的图片加载状态对象if (!loadState) {loadState = [SDWebImageLoadState new];}// 设置加载对象的url为当前的urlloadState.url = url;//将更新后的加载状态对象与当前操作键关联[self sd_setImageLoadState:loadState forKey:validOperationKey];// 从上下文中获取图片管理器,没有就创建一个SDWebImageManager *manager = context[SDWebImageContextCustomManager];if (!manager) {manager = [SDWebImageManager sharedManager];} else {// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)// 从上下文中移除自定义的图片管理器以避免循环引用SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextCustomManager] = nil;context = [mutableContext copy];}BOOL shouldUseWeakCache = NO;if ([manager.imageCache isKindOfClass:SDImageCache.class]) {shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;}if (!(options & SDWebImageDelayPlaceholder)) {//判断是否显示占位图if (shouldUseWeakCache) {NSString *key = [manager cacheKeyForURL:url context:context];// call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query// this unfortunately will cause twice memory cache query, but it's fast enough// in the future the weak cache feature may be re-design or removed[((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];}dispatch_main_async_safe(^{//立即显示占位图[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];});}id <SDWebImageOperation> operation = nil;if (url) {// reset the progress //重制进度追踪NSProgress *imageProgress = loadState.progress;if (imageProgress) {imageProgress.totalUnitCount = 0;imageProgress.completedUnitCount = 0;}#if SD_UIKIT || SD_MAC// check and start image indicator[self sd_startImageIndicator];id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;//启动图片加载小菊花
#endif//设置block回调,用于更新UI以及通知调用者SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {if (imageProgress) {imageProgress.totalUnitCount = expectedSize;imageProgress.completedUnitCount = receivedSize;}
#if SD_UIKIT || SD_MAC//更新小菊花进度if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {double progress = 0;if (expectedSize != 0) {progress = (double)receivedSize / expectedSize;}progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0dispatch_async(dispatch_get_main_queue(), ^{[imageIndicator updateIndicatorProgress:progress];});}
#endif //调用外部进度回调if (progressBlock) {progressBlock(receivedSize, expectedSize, targetURL);}};//弱引用避免循环引用@weakify(self);//开始加载图片operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {@strongify(self);if (!self) { return; }// if the progress not been updated, mark it to complete stateif (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;} //将进度标记为完成状态#if SD_UIKIT || SD_MAC// check and stop image indicator//让小菊花停止if (finished) {[self sd_stopImageIndicator];}
#endif//决定是否调用完成回调BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);//决定是否设置图片BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||(!image && !(options & SDWebImageDelayPlaceholder)));SDWebImageNoParamsBlock callCompletedBlockClosure = ^{if (!self) { return; }if (!shouldNotSetImage) {[self sd_setNeedsLayout]; //设置图片}if (completedBlock && shouldCallCompletedBlock) {completedBlock(image, data, error, cacheType, finished, 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//Case 1a:下载成功,但设置了 不自动设置图片//Case 1b:下载失败,但设置了 不延迟占位图(即立即显示占位图)//不自动设置 image,而是只调用 completedBlock。if (shouldNotSetImage) {dispatch_main_async_safe(callCompletedBlockClosure);return;}//下载成功,自动设置图片或下载失败,延迟显示占位图//使用下载图或使用占位图UIImage *targetImage = nil;NSData *targetData = nil;if (image) {// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not settargetImage = image;targetData = data;} else if (options & SDWebImageDelayPlaceholder) {// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is settargetImage = placeholder;targetData = nil;}#if SD_UIKIT || SD_MAC// check whether we should use the image transition// 检查是否应该使用图片过渡效果。SDWebImageTransition *transition = nil;BOOL shouldUseTransition = NO;if (options & SDWebImageForceTransition) {// AlwaysshouldUseTransition = YES;} else if (cacheType == SDImageCacheTypeNone) {// From networkshouldUseTransition = YES;} else {// From disk (and, user don't use sync query)if (cacheType == SDImageCacheTypeMemory) {shouldUseTransition = NO;} else if (cacheType == SDImageCacheTypeDisk) {if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {shouldUseTransition = NO;} else {shouldUseTransition = YES;}} else {// Not valid cache type, fallbackshouldUseTransition = NO;}}if (finished && shouldUseTransition) {transition = self.sd_imageTransition;}
#endifdispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC[self sd_setImage:targetImage imageData:targetData options:options basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL callback:callCompletedBlockClosure];
#else[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];callCompletedBlockClosure();
#endif});}];[self sd_setImageLoadOperation:operation forKey:validOperationKey];} else {// 如果url无效则立即停止小菊花
#if SD_UIKIT || SD_MAC[self sd_stopImageIndicator];
#endifif (completedBlock) {dispatch_main_async_safe(^{ // 设置回调返回错误NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);});}}return operation;
}
实现的流程我已经通过注释进行了解释,用自然语言将整个过程描述一遍的话就是:
先对URL预处理,以免类型错误,如果是NSString会自动转换
准备上下文
context
,context
是一个配置字典,可以指定缓存策略、解码器、下载器等。取消前一个请求,取消旧的下载任务
加载状态绑定
从context中获取图片管理器SDWebImageManager
显示占位图
图片加载开始,重制进度对象,启动小菊花
启动图片下载,设置block更新小菊花,调用progresssBlock
下载完后根据不同的情况处理图片
如果URL为空:停止小菊花,调用完成block并返回URL无效错误
loadImageWithURL
然后我们来看看loadImageWithURL是怎么实现的
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)urloptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nonnull SDInternalCompletionBlock)completedBlock {// Invoking this method without a completedBlock is pointlessNSAssert(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的类型if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];}
// Prevents app crashing on argument type error like sending NSNull instead of NSURLif (![url isKindOfClass:NSURL.class]) {url = nil;}//创建一个新的操作用于管理这次加载SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];operation.manager = self;
BOOL isFailedUrl = NO;if (url) {//当URL存在时,先检查它是否在访问失败的url列表里SD_LOCK(_failedURLsLock);//加锁防止多个线程访问同一个资源isFailedUrl = [self.failedURLs containsObject:url];SD_UNLOCK(_failedURLsLock);}// Preprocess the options and context arg to decide the final the result for manager//预处理选项和上下文参数确定最终的结果SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
//如果url无效或是失败的url没有设置重试选项,立即调用完成回调if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;//调用完成回调,返回错误信息[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];return operation;//返回操作实例}
//将当前操作添加到正在运行的操作列表中并进行加锁保证线程安全SD_LOCK(_runningOperationsLock);[self.runningOperations addObject:operation];SD_UNLOCK(_runningOperationsLock);// Start the entry to load image from cache, the longest steps are below 启动从缓存中加载图片最长的流程如下// Steps without transformer: 没有变换器的流程, 变换器指的是对图像进行加工的工具// 1. query image from cache, miss 从缓存中查询图像, 如果缓存中没有图像// 2. download data and image 下载数据以及图像// 3. store image to cache 并将其存储到缓存中// Steps with transformer:// 1. query transformed image from cache, miss 从缓存中查询已变换的图像,如果没有// 2. query original image from cache, miss 在缓存中查询原始图像, 如果没有// 3. download data and image 下载数据与图像// 4. do transform in CPU 在CPU中完成转换操作// 5. store original image to cache 将原始图像存储到缓存中// 6. store transformed image to cache 将变换后的图像存储到缓存中[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
我们同样使用自然语言描述一下整个流程:
URL类型检验与转换
初始化加载操作对象,得到
CombinedOperation
,用来标识和管理这次加载任务判断是否是“失败 URL”,避免每次都去请求已经确定失败的地址
生成处理结果对象,统一处理
options
和context
,确保后续所有调用用的是标准格式如果 URL 是空字符串,或者是黑名单 URL 且没有设置重试选项,会直接调用
completedBlock
并返回错误加入运行中操作集合
调用方法callCacheProcessForOperation: url: options: context: progress: completed: 来决定缓存和下载策略并执行
callCacheProcessForOperation
接着我们看看callCacheProcessForOperation这个方法是如何实现的
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operationurl:(nonnull NSURL *)urloptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// Grab the image cache to use//获取需要查询的缓存图像,如果上下文中有则优先从上下文中获取,否则就从当前类中获取id<SDImageCache> imageCache = context[SDWebImageContextImageCache];if (!imageCache) {imageCache = self.imageCache;}// Get the query cache type//获取缓存查询类型,默认查询所有类型的缓存(内存和磁盘)SDImageCacheType queryCacheType = SDImageCacheTypeAll;if (context[SDWebImageContextQueryCacheType]) {queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];}// Check whether we should query cache//检查是否应该查询缓存BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);if (shouldQueryCache) {// transformed cache key// 根据url与上下文生成缓存键NSString *key = [self cacheKeyForURL:url context:context];// to avoid the SDImageCache's sync logic use the mismatched cache key// we should strip the `thumbnail` related context//为了避免SDImageCache的同步逻辑使用不匹配的缓存键,我们需要移除与缩略图相关的上下文SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextImageThumbnailPixelSize] = nil;mutableContext[SDWebImageContextImagePreserveAspectRatio] = nil;@weakify(operation);//查询缓存的操作operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {@strongify(operation);if (!operation || operation.isCancelled) {// 如果操作被取消或是不存在则返回错误// Image combined operation cancelled by user[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];// 安全从运行操作列表中移除操作[self safelyRemoveOperationFromRunning:operation];return;} else if (!cachedImage) { //如果缓存中图片不存在,再去查询原始缓存NSString *originKey = [self originalCacheKeyForURL:url context:context];BOOL mayInOriginalCache = ![key isEqualToString:originKey];// Have a chance to query original cache instead of downloading, then applying transform// Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transformif (mayInOriginalCache) {// 可能存在在原始缓存中,就用原始缓存查询流程[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];return;}}// Continue download process//启用下载流程[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];}];} else {// 直接启用下载流程// Continue download process[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];}
}
同样我们使用自然语言描述,可以对照源码将步骤一一对应:
获取要使用的缓存对象
确定要查询的缓存类型
判断是否需要查询缓存,如果设置了
SDWebImageFromLoaderOnly
选项,就不查询缓存,直接跳到下载流程构造缓存 key:会移除缩略图尺寸等相关信息,避免 key 不一致导致查询失败
执行缓存查询:
如果找到缓存图像,就继续进入下载或处理流程;
如果未找到:
会尝试用原始缓存 key(未做图像变换前的 key)再查一次(这是给例如缩略图、变换图保留原始图缓存的情况);
如果原始 key 也没找到,再进入下载流程。
queryImageForKey
在刚刚函数的实现中,有一行通过queryImageForKey来查询缓存操作:
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {}];
queryImageForKey最后会调用queryCacheOperationForKey,我们来看看它是如何实现的:
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {if (!key) {//如果缓存键为空,则立即完成回调if (doneBlock) {doneBlock(nil, nil, SDImageCacheTypeNone);}return nil;}// Invalid cache type//如果缓存类型为无也立即完成回调if (queryCacheType == SDImageCacheTypeNone) {if (doneBlock) {doneBlock(nil, nil, SDImageCacheTypeNone);}return nil;}// First check the in-memory cache...//首先检查内存缓存UIImage *image;//如果查询类型没有要查询磁盘, 则直接只查询内存if (queryCacheType != SDImageCacheTypeDisk) {image = [self imageFromMemoryCacheForKey:key];}//如果找到了图像if (image) {//只解码第一帧保证图片是静态的if (options & SDImageCacheDecodeFirstFrameOnly) {// Ensure static imageif (image.sd_imageFrameCount > 1) {
#if SD_MACimage = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#elseimage = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif}} else if (options & SDImageCacheMatchAnimatedImageClass) {// Check image class matchingClass animatedImageClass = image.class;Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {image = nil;}}}
//检查是否只需要查询内存,只查询内存的话之后立即回调,不再查询磁盘BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));if (shouldQueryMemoryOnly) {if (doneBlock) {doneBlock(image, nil, SDImageCacheTypeMemory);}return nil;}//接下来查询磁盘缓存// Second check the disk cache...SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];operation.key = key;//用于查询对象operation.callbackQueue = queue;//设置操作队列// Check whether we need to synchronously query disk// 1. in-memory cache hit & memoryDataSync// 2. in-memory cache miss & diskDataSync//根据是否需要同步处理,选择同步或异步查询磁盘BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||(!image && options & SDImageCacheQueryDiskDataSync));//定义从磁盘查询数据的BlockNSData* (^queryDiskDataBlock)(void) = ^NSData* {//定义Block,对取消操作进行加锁@synchronized (operation) {if (operation.isCancelled) {return nil;}}//如果操作没有被取消,从所有可能路径中搜索数据return [self diskImageDataBySearchingAllPathsForKey:key];};//定义从磁盘创建图像的BlockUIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {@synchronized (operation) {if (operation.isCancelled) {return nil;}}UIImage *diskImage;if (image) {// the image is from in-memory cache, but need image data//如果已经在内存中找到图像,但是需要图像数据diskImage = image;} else if (diskData) {BOOL shouldCacheToMemory = YES;if (context[SDWebImageContextStoreCacheType]) {//检查是否应该将图像缓存到内存中SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];shouldCacheToMemory = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);}//// 特殊情况:如果用户查询同一URL的图像以避免多次解码和写入相同的图像对象到磁盘缓存中,我们在这里再次查询和检查内存缓存// Special case: If user query image in list for the same URL, to avoid decode and write **same** image object into disk cache multiple times, we query and check memory cache here again.if (shouldCacheToMemory && self.config.shouldCacheImagesInMemory) {diskImage = [self.memoryCache objectForKey:key];}// decode image data only if in-memory cache missed//如果内存缓存未命中,解码磁盘数据if (!diskImage) {diskImage = [self diskImageForKey:key data:diskData options:options context:context];// check if we need sync logicif (shouldCacheToMemory) {[self _syncDiskToMemoryWithImage:diskImage forKey:key];}}}return diskImage;};// Query in ioQueue to keep IO-safe// 用IO队列保证IO操作安全// 同步执行磁盘查询if (shouldQueryDiskSync) {__block NSData* diskData;__block UIImage* diskImage;dispatch_sync(self.ioQueue, ^{diskData = queryDiskDataBlock();diskImage = queryDiskImageBlock(diskData);});if (doneBlock) {doneBlock(diskImage, diskData, SDImageCacheTypeDisk);}} else {//异步执行查询操作dispatch_async(self.ioQueue, ^{NSData* diskData = queryDiskDataBlock();UIImage* diskImage = queryDiskImageBlock(diskData);@synchronized (operation) {if (operation.isCancelled) {return;}}if (doneBlock) {[(queue ?: SDCallbackQueue.mainQueue) async:^{// Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing// This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)@synchronized (operation) {if (operation.isCancelled) {return;}}doneBlock(diskImage, diskData, SDImageCacheTypeDisk);}];}});}return operation;
}
使用自然语言描述:
校验key和缓存类型
尝试查询内存缓存:如果设置的
cacheType
不是.Disk
,就尝试从内存中获取图片。如果找到了图片:如果设置了只解码第一帧(针对动图),会将动图转成静态图。
如果设置了需要匹配特定图片类(如动图类),但类型不匹配,则丢弃这个图片。
判断是否只需要查询内存
准备磁盘查询操作
同步或异步执行磁盘查询:如果设置了同步查询磁盘的选项,则在 IO 队列中同步读取磁盘并执行回调;否则异步查询:
异步从磁盘读取数据并解码成图片。
最后将结果切回主线程或指定的回调队列进行回调。
在读取数据、解码图片和回调之前,都会判断是否调用了取消操作(通过
operation.isCancelled
)来提前退出,避免多余工作。
可见这个方法的作用是:
根据指定的 key(缓存键),从内存或磁盘中查询图片缓存,并通过回调返回结果(UIImage 和 image data)。支持多种查询选项,比如只查询内存、是否解码第一帧、是否匹配特定图片类等。返回一个 SDImageCacheToken
,用于后续可能的取消操作。
callDownloadProcessForOperation
这个方法负责在缓存查找完成后,决定是否从网络下载图片并执行相关回调
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operationurl:(nonnull NSURL *)urloptions:(SDWebImageOptions)optionscontext:(SDWebImageContext *)contextcachedImage:(nullable UIImage *)cachedImagecachedData:(nullable NSData *)cachedDatacacheType:(SDImageCacheType)cacheTypeprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// Mark the cache operation end//标记缓存操作结束@synchronized (operation) {operation.cacheOperation = nil;}// Grab the image loader to use//获取默认加载器id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];if (!imageLoader) {imageLoader = self.imageLoader;}// Check whether we should download image from network//检查是否需要从网上下载图片BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);//如果需要刷新缓存或者缓存中没有图像shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);//委托是否允许下载if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context];} else {shouldDownload &= [imageLoader canRequestImageForURL:url];}if (shouldDownload) {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:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.SDWebImageMutableContext *mutableContext;if (context) {mutableContext = [context mutableCopy];} else {mutableContext = [NSMutableDictionary dictionary];}mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;context = [mutableContext copy];}@weakify(operation);//发起图像下载请求operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {@strongify(operation);if (!operation || operation.isCancelled) {//如果操作被取消返回错误信息// Image combined operation cancelled by user[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {// Image refresh hit the NSURLCache cache, do not call the completion block} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {// Download operation cancelled by user before sending the request, don't block failed URL[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];} else if (error) {[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];//向错误集合中添加当前错误if (shouldBlockFailedURL) {SD_LOCK(self->_failedURLsLock);[self.failedURLs addObject:url];SD_UNLOCK(self->_failedURLsLock);}} else {if ((options & SDWebImageRetryFailed)) {SD_LOCK(self->_failedURLsLock);[self.failedURLs removeObject:url];SD_UNLOCK(self->_failedURLsLock);}// Continue transform process//继续图像转换流程,同时保存图像到缓存中[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];}if (finished) {//完成后在当前操作列表中移除当前操作[self safelyRemoveOperationFromRunning:operation];}}];} else if (cachedImage) {//如果不下载且缓存中有图像,则使用缓存的图像[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];[self safelyRemoveOperationFromRunning:operation];} else {//图像不在缓存中,也不允许下载// Image not in cache and download disallowed by delegate[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];[self safelyRemoveOperationFromRunning:operation];}
}
使用自然语言描述如下:
标记缓存操作已结束
获取图片加载器(imageLoader)
判断是否需要下载图片
如果需要下载,进入下载流程:
如果缓存中已有图片且设置了刷新缓存(
RefreshCached
):先立即回调一次缓存图像,让用户界面立即显示;
同时继续向服务器发起请求,用于刷新或确认是否真的有新内容;
把缓存图像传给加载器,用于与远程图像比较,避免重复下载。
发起下载请求
通过
imageLoader
执行网络请求,传入 URL、选项、上下文等;请求完成后会回调到一个
completed:
block。
处理下载完成回调:
如果下载被禁止,但已经命中缓存,则直接使用缓存图像回调并移除任务
如果不下载也没有缓存图像,直接回调空图像,表示整个请求失败或被禁止,任务结束
storeImage
在执行完下载后,会继续执行转换与缓存处理,这里我们不关注转换操作,将目光聚集到保存操作,保存操作的核心是storeImage,搜索storeImage可以看到它的实现:
将图像存储到内存缓存
这里判断是否继续存储到磁盘,如果不需要存储到磁盘,就调用完成回调并返回
这一段将数据存储到磁盘中
setImage
下载成功后,经过重重回调,要回调的数据沿着SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache
一路流动,其中流动到SDWebImageManager
中时对图片进行了缓存,最后在UIView+WebCache
中为UIImageView
设置了处理好的图片。
可以在sd_internalSetImageWithURL
方法中看到,在更新一系列外部配置像图片过度效果等后,会在主线程调用sd_setImage
更新UI
sd_setImage:
可以看到这里通过判断类是button还是imageView来设置不同的设置方法
总结
由此总结一下SDWebImage的调用流程:
首先我们会进入setImagewithURL:方法中,然后进入sd-InternalmageWithURL方法中,在这个方法中我们首先通过validOperationKey取消正在运行的任务,任务是通过sd_cancelImageLoadOperationWithKey方法取消的,这一步是为了避免同一资源被重复下载,接着我们初始化SDWebManager(这里因为SDWebManager是单例,所以只初始化一次),接着进行一系列配置后调用loadImageWithURL方法,首先检查URL是否在错误的集合中,如果没有就调用queryImageForKey去查找缓存,查找缓存的步骤是首先查找内存缓存,内存缓存找不到再去查找磁盘缓存,都找不到则去查询原始数据。如果都找不到我们就去执行下载操作,下载操作完成后通过storeImage方法将图像存储到缓存中,最后回到SDWebImageManager单例类中通过setImage方法将Image设置在对应的视图上