【iOS】SDWebImage源码学习
SDWebImage源码学习
文章目录
- SDWebImage源码学习
- 前言
- SDWebImage缓存流程
- sd_setImageWithURL(UIImageView+WebCache层)
- sd_internalSetImageWithURL(UIView+WebCache层)
- loadImageWithURL(SDWebManger层)
- queryCacheOperationForKey(SDImageCache层)
- 删除缓存
- callDownloadProcessForOperation(SDWebImagerManger层)
- downloadImageWithURL(SDImageLoadersManager层)
- storeImage(SDImageCache)
- setImage(SDWebImageManager)
前言
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。
SDWebImage这个第三方库有以下几个优势:
- 一个异步的图片加载器。
- 一个异步的内存+磁盘图片缓存
- 支持GIF、WebP图片
- 后台图片解压缩处理
- 确保同一个URL的图片不被多次下载
- 确保非法的URL不会被反复加载
- 确保下载及缓存时,主线程不被阻塞.
首先我们先看一下有关于SDWebImage官方给出的一个图:
下面我们主要讲一下这几个比较重要的对象
-
UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调
-
SDWebImageManager,对图片进行管理的中转站,记录哪些图片正在读取
- 向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader)。
- 实现SDImageCache和SDWebImageDownloader的回调
-
SDImageCache
- 根据URL的MD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
- 实现图片和内存清理工作
-
SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)
SDWebImage缓存流程
SDWebImage缓存流程
这里我们先看一下流程图:
这时候我们在用文字讲解一下流程:
sd_setImageWithURL(UIImageView+WebCache层)
- 现在某一个对象上的
SDWebImage
这个库中调用sd_setImageWithURL
这个方法,然后我们点进去看一下他的一个具体调用:
这里发现它是一个逐级调用一个完整的set方法,所以所有方法最后都会调用下面这个方法:
- (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;
这时候我们在看这个函数的一个逻辑:
sd_internalSetImageWithURL(UIView+WebCache层)
-
处理了参数的安全性质:
- 将
NSString
类型的 URL 转换为NSURL
。 - 过滤非法 URL(如
NSNull
),确保后续流程安全。 - 生成唯一的
validOperationKey
(默认为当前类名),用于标识和管理当前图片加载操作。(会将url作为属性绑定到UIView上)
if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];//SDWebImagey允许传入一个}// Prevents app crashing on argument type error like sending NSNull instead of NSURLif (![url isKindOfClass:NSURL.class]) {url = nil;}//前面处理一个参数的安全性质if (context) {// copy to avoid mutable object//创建一个副本避免直接修改原来的值context = [context copy];} else {context = [NSDictionary dictionary];}NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; // 生成一个唯一的key来管理当前图片加载操作 后面也会将url作为属性绑定到UIView上
- 将
-
取消历史操作,保证一个单线程
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; //获得对应的一个keyif (!validOperationKey) { //当 validOperationKey 为空时(即用户未指定),进入 if 块生成默认key// pass through the operation key to downstream, which can used for tracing operation or image view class为了不直接修改传入的上下文对象(这可能会影响其他地方的使用),首先对其进行深复制,得到一个可修改的副本mutableContext。然后在这个副本中设置新的操作键(无论是用户自定义的还是当前类名)。最后,将修改后的可变上下文(mutableContext)再次复制成一个不可变字典,替换原先的context对象,以供后续操作使用。validOperationKey = NSStringFromClass([self class]);SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;context = [mutableContext copy];}// 默认情况下,如果没有设置SDWebImageAvoidAutoCancelImage选项,则取消与当前设置图片操作键相关联的所有先前的下载操作。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];} // 在 UI 开发中,尤其是在列表或滚动视图中,用户可能快速滚动,视图复用机制会导致视图的内容需要频繁更新。如果不取消先前的下载操作,就可能出现以下问题: // 性能问题:同时进行多个不必要的下载任务,增加内存和网络的负担。 // 数据错误:旧的下载任务可能后于新的任务完成,导致视图上显示的图片是错误的。- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {if (!key) {key = NSStringFromClass(self.class); // 如果key为nil就用类生成一个key}// Cancel in progress downloader from queue//取消任务SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; //获得所有的一个OperationsDictionaryid<SDWebImageOperation> operation;@synchronized (self) { //互斥锁保证他的一个安全性operation = [operationDictionary objectForKey:key];}if (operation) {if ([operation respondsToSelector:@selector(cancel)]) { //有这个方法就移除[operation cancel];}@synchronized (self) {[operationDictionary removeObjectForKey:key]; //移除一个队列字典中的队列}} }
-
初始化一个manger
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];}
-
判断是否需要一个弱缓存,并且根据placeholder显示图片
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];});}
-
判断url是否存在,如果存在就开始正式的一个加载流程,重置
NSProgress
、SDWebImageIndicator,
完成下方配置的时候,进入我们的loadImageWithURL
函数,当下载完成的时候根据需要决定有没有过度图片,加载结束后通过Block返回图片就可以了.if (url) {// reset the progressNSProgress *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//设置一个回调函数SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {if (imageProgress) {imageProgress.totalUnitCount = expectedSize;imageProgress.completedUnitCount = receivedSize;} #if SD_UIKIT || SD_MACif ([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];});} #endifif (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) { //加载图片 (里面采用了强弱共舞的方式保证了block不会发生循环引用)//省略部分代码[self sd_setImageLoadOperation:operation forKey:validOperationKey];//设置image的一个operation//``````}
loadImageWithURL(SDWebManger层)
loadImageWithURL
是SDWebImageManager
中的方法,SDWebImageManager是一个单例,在初始化的时候,还初始化了SDImageCache
和SDWebImageDownloader
。SDWebImageManager的作用就是调度SDImageCache和SDWebImageDownloader进行缓存和下载操作的。
- 参数校验,和前面的方法一样:
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;}
- 创建操作对象,用来管理下载和缓存的部分:把manger关联上,方便后面的处理
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];operation.manager = self;
- 判断是否在失败的URL中,防止多次失败下载:
- 这里通过递归锁保证了对共享资源的安全访问,共享资源就是
self.failedURLs
,之所以加锁是为了防止其他线程访问当前集合并对其进行修改
- 这里通过递归锁保证了对共享资源的安全访问,共享资源就是
BOOL isFailedUrl = NO;if (url) {//如果url存在就判断一次SD_LOCK(_failedURLsLock); //加锁避免多线程访问统一块内存isFailedUrl = [self.failedURLs containsObject:url]; SD_UNLOCK(_failedURLsLock);}
- 如果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);
最后进入callCacheProcessForOperation
这个函数,这个函数里面会调用queryImageForKey
,通过queryImageForKey
再调用queryCacheOperationForKey
这个函数
queryCacheOperationForKey(SDImageCache层)
这个函数最重要的是查找缓存的内容,
先来看一下有关缓存策略的配置的一个类别,保存缓存策略的信息,比如最长的缓存时间去清除缓存:
@implementation SDImageCacheConfig
- (instancetype)init {if (self = [super init]) {_shouldDisableiCloud = YES;_shouldCacheImagesInMemory = YES;_shouldUseWeakMemoryCache = NO;_shouldRemoveExpiredDataWhenEnterBackground = YES;_shouldRemoveExpiredDataWhenTerminate = YES;_diskCacheReadingOptions = 0;_diskCacheWritingOptions = NSDataWritingAtomic;_maxDiskAge = kDefaultCacheMaxDiskAge;_maxDiskSize = 0;_diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;_fileManager = nil;if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) {_ioQueueAttributes = DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL; // DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM} else {_ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL}_memoryCacheClass = [SDMemoryCache class];_diskCacheClass = [SDDiskCache class];}return self;
}
继续看一下SDImageCache
中关于缓存策略的几个枚举:
typedef NS_ENUM(NSInteger, SDImageCacheType) {/*** For query and contains op in response, means the image isn't available in the image cache* For op in request, this type is not available and take no effect.*///图片需要从网络上下载SDImageCacheTypeNone,/*** For query and contains op in response, means the image was obtained from the disk cache.* For op in request, means process only disk cache.*///表示图片来源于磁盘缓存SDImageCacheTypeDisk,/*** For query and contains op in response, means the image was obtained from the memory cache.* For op in request, means process only memory cache.*///表示图片来源内存缓存SDImageCacheTypeMemory,/*** For query and contains op in response, this type is not available and take no effect.* For op in request, means process both memory cache and disk cache.*///表示请求操作,表示同时查询寻内存和磁盘缓存SDImageCacheTypeAll
};
下面就进入我们的SDImageCache
中的初始化操作,SDImageCache
是在Manger
初始化的时候就完成了初始化,现在看一下初始化过程中具体做了什么操作:
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)nsdiskCacheDirectory:(nullable NSString *)directoryconfig:(nullable SDImageCacheConfig *)config {if ((self = [super init])) {NSAssert(ns, @"Cache namespace should not be nil");if (!config) {config = SDImageCacheConfig.defaultCacheConfig;}_config = [config copy];// Create IO queue//创建一个执行IO操作的串行队列,以及NSFileManger的初始化dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes;_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes);NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong");// Init the memory cacheNSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol");_memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];//构造SDImageCacheConfig配置对象// Init the disk cacheif (!directory) {// Use default disk cache directorydirectory = [self.class defaultDiskCacheDirectory];}_diskCachePath = [directory stringByAppendingPathComponent:ns];NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol");//设置内存缓存的name和磁盘缓存的文件路径_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];// Check and migrate disk cache directory if need[self migrateDiskCacheDirectory];#if SD_UIKIT// Subscribe to app events//添加应用即将结束和进入后台的通知[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(applicationWillTerminate:)name:UIApplicationWillTerminateNotificationobject:nil];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(applicationDidEnterBackground:)name:UIApplicationDidEnterBackgroundNotificationobject:nil];
#endif
#if SD_MAC[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(applicationWillTerminate:)name:NSApplicationWillTerminateNotificationobject:nil];
#endif}return self;
}
为缓存做了充足的准备,下面正式存储和查找的流程
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {// 如果key为空,就立刻nil并且调用完成回调if (!key) {if (doneBlock) {doneBlock(nil, nil, SDImageCacheTypeNone);}return nil; //key确保有效且queryCacheType合法.避免无效查询}// 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]; //通过NSCache快速查询内存缓存}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 matching//根据上下问中的齐王动画图片类型进行一个检查Class 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];};// 定义从磁盘加载创建的图象的Block,根据磁盘数据生成一个image对象UIImage* (^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//图片依从内存缓存获取,仅需根据数据生成UIImagediskImage = image;} else if (diskData) { //从磁盘获取数据,需要解析位UIImageBOOL shouldCacheToMemory = YES;if (context[SDWebImageContextStoreCacheType]) {SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];shouldCacheToMemory = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);}// 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-safeif (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;
}
上面代码主要就做了这几件事情:
- 先检查键值是否为空并且图片leix是否哦合法,如果不为空并且合法的情况在执行下米纳的操作,否则就直接执行回调
- 在内粗那种查找缓存,如果找到了就图象就先处理第一帧来保证图片是静态的
- 检查是否只需要查询内存,如果质用查询内存的话就立即回调
- 下面选择异步还是同步查询磁盘缓存
- 磁盘中且内存中找到缓存的话就开始解析图片.如果内存中没有缓存就会先从磁盘传向内存中,如果磁盘中也没有找到缓存的画家就开始执行下载任务,如果要执行下载在操作的话就是调用
SDWebManger
层的callDownloadProcessForOperation
删除缓存
-
需要一个文件的迭代器fileEnumerator去获取需要删除的图片文件
-
根据前面设置的清除缓存的策略中最大时间限制(默认一周),计算出文件(创建或上一次修改)需要被清除的时间范围
-
根据这个时间范围遍历出需要删除的图片,然后从磁盘中删除
-
再根据缓存策略中配置的最大缓存大小,判断时候需要进一步清除缓存
-
按创建的先后顺序继续删除缓存,直到缓存大小是最大值的一半
callDownloadProcessForOperation(SDWebImagerManger层)
这个方法的作用就是在下载图片
- (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// 获取图片加载器,优先使用上下文提供的,否则使用默认的imageLoader属性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];}
}
-
判断是否需要从网络下载图片;
-
若要下载,则发起下载请求;
-
处理下载结果(成功、失败、取消);
-
回调对应的
completedBlock
; -
清理状态。
这里通过这个函数来正式开始从网络上加载图片:
requestImageWithURL
这个函数会调用downloadImageWithURL
这才算是我们下载的核心函数:
downloadImageWithURL(SDImageLoadersManager层)
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)urloptions:(SDWebImageDownloaderOptions)optionscontext:(nullable SDWebImageContext *)contextprogress:(nullable SDWebImageDownloaderProgressBlock)progressBlockcompleted:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {// 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.//处理urlif (url == nil) {if (completedBlock) {NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];completedBlock(nil, nil, error, YES);}return nil;}//初始化下载操作取消令牌,用于取消关联的下载操作id downloadOperationCancelToken;// When different thumbnail size download with same url, we need to make sure each callback called with desired size//根据上下文德缓存key生成缓存建用于唯一标识图片资源id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];NSString *cacheKey;if (cacheKeyFilter) {cacheKey = [cacheKeyFilter cacheKeyForURL:url];} else {cacheKey = url.absoluteString;}//根据上下文和下载选项生成解码选项SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey); //加锁SD_LOCK(_operationsLock); //下载操作的复用和创建NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];// There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.//检查是否可以复用现有的下载操作BOOL shouldNotReuseOperation;if (operation) {@synchronized (operation) {shouldNotReuseOperation = operation.isFinished || operation.isCancelled;}} else {shouldNotReuseOperation = YES;}if (shouldNotReuseOperation) {//创建新的下载操作 operation = [self createDownloaderOperationWithUrl:url options:options context:context];if (!operation) {SD_UNLOCK(_operationsLock);if (completedBlock) {NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];completedBlock(nil, nil, error, YES);}return nil;}//设置操作完成的时候从操作字典移除这个操作@weakify(self);operation.completionBlock = ^{@strongify(self);if (!self) {return;}SD_LOCK(self->_operationsLock);[self.URLOperations removeObjectForKey:url];SD_UNLOCK(self->_operationsLock);};[self.URLOperations setObject:operation forKey:url]; //设置字典对应的key和value// Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];// 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.[self.downloadQueue addOperation:operation];} else {// When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.@synchronized (operation) {downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];}}SD_UNLOCK(_operationsLock);SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];token.url = url;token.request = operation.request;token.downloadOperationCancelToken = downloadOperationCancelToken;return token;
}
这个方法主要是为了图片下载任务提供一个统一的入口,管理下载操作的生命周期,回调处理和线程安全.这里的createDownloaderOperationWithUrl
这个函数是真正触发下载的流程.在执行完成后返回一个operation通过operation进行后面的操作最后返回一个token。
这个返回类型这么做的一个目的就是可以在取消的回调中及时取消下载操作.
storeImage(SDImageCache)
完成下载后,会回到Mnager层级然后在调用Cache层级的storeImage函数把图片保存到我们的cache中.,先会保存道内存中,如果还需要保存到磁盘中那就再深入一级保存到磁盘中
setImage(SDWebImageManager)
这里我们返回到这里通过这个单例类来将image设置到对应的一个视图上.