前言

之前在写项目时,经常用到SDWebImage这个第三方库来加载图片,并且了解到了这个第三方库在处理图片时自带异步下载和缓存功能,以及对cell复用的处理。这篇文章来系统学习一下SDWebImage第三方库的知识以及底层原理

简介

SDWebImageUIImageViewUIButton提供了下载分类,可以很简单地实现图片异步下载与缓存功能。SDWebImage的第三方库具有以下特性

  1. 异步下载图片

  2. 异步缓存(内存+磁盘),自动管理缓存有效性

  3. 同一个URL不会重复下载

  4. 自动识别无效URL,不会反复重试

  5. 不阻塞主线程

  6. 使用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;
}

实现的流程我已经通过注释进行了解释,用自然语言将整个过程描述一遍的话就是:

  1. 先对URL预处理,以免类型错误,如果是NSString会自动转换

  2. 准备上下文contextcontext 是一个配置字典,可以指定缓存策略、解码器、下载器等。

  3. 取消前一个请求,取消旧的下载任务

  4. 加载状态绑定

  5. 从context中获取图片管理器SDWebImageManager

  6. 显示占位图

  7. 图片加载开始,重制进度对象,启动小菊花

  8. 启动图片下载,设置block更新小菊花,调用progresssBlock

  9. 下载完后根据不同的情况处理图片

如果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;
}

我们同样使用自然语言描述一下整个流程:

  1. URL类型检验与转换

  2. 初始化加载操作对象,得到CombinedOperation,用来标识和管理这次加载任务

  3. 判断是否是“失败 URL”,避免每次都去请求已经确定失败的地址

  4. 生成处理结果对象,统一处理 optionscontext,确保后续所有调用用的是标准格式

  5. 如果 URL 是空字符串,或者是黑名单 URL 且没有设置重试选项,会直接调用 completedBlock 并返回错误

  6. 加入运行中操作集合

  7. 调用方法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];}
}

同样我们使用自然语言描述,可以对照源码将步骤一一对应:

  1. 获取要使用的缓存对象

  2. 确定要查询的缓存类型

  3. 判断是否需要查询缓存,如果设置了 SDWebImageFromLoaderOnly 选项,就不查询缓存,直接跳到下载流程

  4. 构造缓存 key:会移除缩略图尺寸等相关信息,避免 key 不一致导致查询失败

  5. 执行缓存查询:

    1. 如果找到缓存图像,就继续进入下载或处理流程;

    2. 如果未找到:

      1. 会尝试用原始缓存 key(未做图像变换前的 key)再查一次(这是给例如缩略图、变换图保留原始图缓存的情况);

      2. 如果原始 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;
}

使用自然语言描述:

  1. 校验key和缓存类型

  2. 尝试查询内存缓存:如果设置的 cacheType 不是 .Disk,就尝试从内存中获取图片。如果找到了图片:

    • 如果设置了只解码第一帧(针对动图),会将动图转成静态图。

    • 如果设置了需要匹配特定图片类(如动图类),但类型不匹配,则丢弃这个图片。

  3. 判断是否只需要查询内存

  4. 准备磁盘查询操作

  5. 同步或异步执行磁盘查询:如果设置了同步查询磁盘的选项,则在 IO 队列中同步读取磁盘并执行回调;否则异步查询:

    • 异步从磁盘读取数据并解码成图片。

    • 最后将结果切回主线程或指定的回调队列进行回调。

  6. 在读取数据、解码图片和回调之前,都会判断是否调用了取消操作(通过 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];}
}

使用自然语言描述如下:

  1. 标记缓存操作已结束

  2. 获取图片加载器(imageLoader)

  3. 判断是否需要下载图片

  4. 如果需要下载,进入下载流程:

    1. 如果缓存中已有图片且设置了刷新缓存(RefreshCached):

      1. 先立即回调一次缓存图像,让用户界面立即显示;

      2. 同时继续向服务器发起请求,用于刷新或确认是否真的有新内容;

      3. 把缓存图像传给加载器,用于与远程图像比较,避免重复下载。

    2. 发起下载请求

      1. 通过 imageLoader 执行网络请求,传入 URL、选项、上下文等;

      2. 请求完成后会回调到一个 completed: block。

  5. 处理下载完成回调:

  6. 如果下载被禁止,但已经命中缓存,则直接使用缓存图像回调并移除任务

  7. 如果不下载也没有缓存图像,直接回调空图像,表示整个请求失败或被禁止,任务结束

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设置在对应的视图上

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/94320.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/94320.shtml
英文地址,请注明出处:http://en.pswp.cn/web/94320.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux --网络基础概念

一.网络发展独立模式&#xff1a;在早期计算机之间是相互独立的&#xff0c;机器之间的数据只能通过软硬盘来传输&#xff0c;这就代表无法同时完成任务&#xff0c;需要前面的计算机完成各自的任务经过硬盘传递数据再完成自己的任务&#xff0c;效率十分低下。网络互联&#x…

教育系统搭建攻略:线上知识付费与线下消课排课全解析

作为一名资深平台测评师&#xff0c;最近我挖到了一个教育机构的 “宝藏工具”—— 乔拓云教育系统。别看它名字低调&#xff0c;用起来那叫一个顺手&#xff0c;线上知识付费、线下消课排课全给你安排得明明白白&#xff0c;简直是机构老板和教务员的 “摸鱼神器”。多端口管理…

PMP项目管理知识点-①项目基本概念

目录 1.项⽬的定义 概念&#xff1a; 特点&#xff1a; 项⽬与运营的区别 项⽬特点&#xff1a; 运营特点&#xff1a; 2.项⽬管理的发展 3.项⽬、项⽬集与项⽬组合 结构层次 4.项⽬的关键组成部分 项⽬⽣命周期&#xff1a; 项⽬管理过程组&#xff1a; 项⽬阶段&…

Python内置函数全解析:30个核心函数语法、案例与最佳实践指南

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 持续学习&#xff0c;不断…

数据建模怎么做?一文讲清数据建模全流程

目录 一、需求分析 1. 搞清楚业务目标&#xff1a;这数据是要解决啥问题&#xff1f; 2. 明确数据边界&#xff1a;哪些数据该要&#xff0c;哪些不该要&#xff1f; 3. 弄明白使用场景&#xff1a;谁用这数据&#xff0c;怎么用&#xff1f; 二、模型设计 1. 第一步&…

胸部X光片数据集:健康及肺炎2类,14k+图像

胸部X光片数据集概述 数据集包含14090张图像,分为正常胸部X光3901张,肺炎胸部X光10189张。 标注格式:无标注,文件夹分类。 图像尺寸:640*640 正常胸部X光: 肺炎胸部X光: 数据采集: 拍摄方式:均为前后位(anterior-posterior)胸部X光,属患者常规临床护理的一部分…

MySQL數據庫開發教學(二) 核心概念、重要指令

書接上回&#xff1a;MySQL數據庫開發教學(一) 基本架構-CSDN博客 建議工具&#xff1a; Navicat Premium (收費 / 需破解)&#xff1a;Navicat Premium | 管理和开发你的数据库 phpstudy 2018 (免費)&#xff1a;phpStudy - Windows 一键部署 PHP 开发环境 小皮出品 前言 …

【40页PPT】数字工厂一体化运营管控平台解决方案(附下载方式)

篇幅所限&#xff0c;本文只提供部分资料内容&#xff0c;完整资料请看下面链接 https://download.csdn.net/download/2501_92808811/91716541 资料解读&#xff1a;【40页PPT】数字工厂一体化运营管控平台解决方案 详细资料请看本解读文章的最后内容。该资料围绕数字工厂一体…

数据产品(2)用户画像数据分析模型

目录 1 用户画像 2 RFM模型 (用户价值分群模型) 3 PSM 价格敏感度 4 精细化运营 1 用户画像 也称用户表标签,是基于用户行为分析获得的对用户的一种认知表达,即用户数据标签化,通过收集与分析用户的用户属性(年龄、性别、城市、职业、设备、状态)、用户偏好(购物偏好,听…

03_数据结构

第3课&#xff1a;数据结构 课程目标 掌握Python的基本数据结构&#xff1a;列表、元组、字典、集合学习字符串的高级操作方法理解不同数据结构的特点和适用场景 1. 列表&#xff08;List&#xff09; 1.1 列表的创建和基本操作 # 创建列表 fruits ["苹果", "香…

【JavaEE】多线程 -- CAS机制(比较并交换)

目录CAS是什么CAS的应用实现原子类实现自旋锁ABA问题ABA问题概述ABA问题引起的BUG解决方案CAS是什么 CAS (compare and swap) 比较并交换&#xff0c;CAS 是物理层次支持程序的原子操作。说起原子性&#xff0c;这就设计到线程安全问题&#xff0c;在代码的层面为了解决多线程…

The United Nations Is Already Dead

The United Nations Is Already Dead When children in Gaza rummage through rubble for food, when UN-run schools are reduced to dust, when the Security Council cannot even pass the mildest ceasefire resolution—blocked by a single veto— we must confront a br…

Kubernetes v1.34 前瞻:资源管理、安全与可观测性的全面进化

预计正式发布&#xff1a;2025年8月底 | 分类&#xff1a;Kubernetes 随着2025年8月底的临近&#xff0c;Kubernetes社区正紧锣密鼓地准备下一个重要版本——v1.34的发布。本次更新并非简单的功能叠加&#xff0c;而是在资源管理、安全身份、可观测性和工作负载控制等核心领域的…

用 Bright Data MCP Server 构建实时数据驱动的 AI 情报系统:从市场调研到技术追踪的自动化实战

前言 本文通过两个真实场景&#xff08;云服务商对比与 AIGC 技术追踪&#xff09;&#xff0c;展示了如何使用 Bright Data MCP Server 与 Lingma IDE 构建一个具备实时网页数据抓取、结构化分析与自动化报告生成能力的 AI 工作流。通过简单的 API 调用与 JSON 配置&#xff…

牛顿第二定律的所有表达方式:1、线性表达 2、圆形表达 3、双曲线表达 4、抛物线表达5、数列表达

牛顿第二定律是经典力学中的核心定律&#xff0c;表述为&#xff1a;物体的加速度与所受合力成正比&#xff0c;与质量成反比&#xff0c;方向与合力方向相同。其基本矢量形式为&#xff1a; F⃗ma⃗ \vec{F} m \vec{a} Fma 其中&#xff0c;F⃗\vec{F}F 是合力&#xff08;单…

【开发日记】SpringBoot 实现支持多个微信小程序的登录

在实际业务场景中&#xff0c;需要一个后台同时支持多个微信小程序的登录。例如&#xff0c;企业有多个不同业务的小程序&#xff0c;但希望统一在同一个后台系统里进行用户认证和数据处理。这时候&#xff0c;我们就需要一个灵活的方式来管理多个小程序的 appid 和 secret&…

Docker 容器(一)

Docker一、Docker是什么1.什么是Docker2.Docker特点3.比较虚拟机和容器二、Docker安装1.Docker​​三大核心组件​​2.安装步骤&#xff08;Ubuntu&#xff09;3.阿里云镜像加速三、Docker镜像1.什么是镜像2.UnionFS&#xff08;联合文件系统&#xff09;3.Docker镜像加载原理4…

容器安全实践(二):实践篇 - 从 `Dockerfile` 到 Pod 的权限深耕

在上一篇《容器安全实践&#xff08;一&#xff09;&#xff1a;概念篇》中&#xff0c;我们深入探讨了容器安全的底层原理&#xff0c;并纠正了“容器天生安全”的误解。我们了解了 root 用户的双重身份&#xff0c;以及特权容器的危险性。 然而&#xff0c;仅仅了解这些概念…

c#_数据持久化

数据持久化架构 数据是应用程序的命脉。持久化架构的选择直接决定了应用的性能、可扩展性、复杂度和维护成本。本章将深入探讨.NET生态中主流的数据访问模式、工具和策略&#xff0c;帮助你为你的系统做出最明智的数据决策。5.1 ORM之争&#xff1a;Entity Framework Core深度剖…

996引擎-骰子功能

996引擎-骰子功能 测试NPC QF回调函数 结果 参考资料 在测试NPC播放骰子动画。 播放前需要先设置骰子点数 测试NPC [[骰子的显示顺序和点数 对应 私人变量 D0 D1 D2 D3 D4 D5]] -- NPC入口函数 function main(player)-- 骰子共6个,设置骰子点数后,再执行摇骰子,否则没动画…