【iOS】SDWebImage源码阅读笔记

文章目录


前言

最近基本已经将项目完结,这个月开始专心学习源码,将会陆续学习SDWebImage,AFNetworking以及JsonModel的源码


一、设计目的

SDWebImageUIImageViewUIButton提供了下载分类,使我们只需要一行代码即可以实现图片异步下载与缓存功能

二、特性

  1. 异步下载图片
  2. 异步缓存(内存+磁盘),自动管理缓存有效性
  3. 同一个URL不会重复下载
  4. 自动识别无效URL,不会反复重试
  5. 不阻塞主线程
  6. 使用GCD与ARC

三、用法

1.UITableView 中使用 UIImageView+WebCache

UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可


2.使用回调Blocks

在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调

bash 复制代码
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                             completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                    ... completion code here ...
                                 }];

3.SDWebImageManager 的使用

SDWebImageManager是一个单例类,也是SD中的核心类,负责下载与缓存的处理

bash 复制代码
+ (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也可以单独使用。

bash 复制代码
SDWebImageManager *manager = [SDWebImageManager sharedManager];
    [manager loadImageWithURL:imageURL
                      options:0
                     progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                            // progression tracking code
                     }
                     completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                        if (image) {
                            // do something with image
                        }
                     }];

4.单独使用 SDWebImageDownloader 异步下载图片

我们还可以单独使用SDWebImageDownloader来下载图片,但是图片内容不会缓存到磁盘或是内存

bash 复制代码
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    [downloader downloadImageWithURL:imageURL
                             options:0
                            progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                // progression tracking code
                            }
                           completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                if (image && finished) {
                                    // do something with image
                                }
                            }];

5.单独使用 SDImageCache 异步缓存图片

SDWebImage支持内存缓存与异步的磁盘缓存(可选),如果我们想用SDImageCache来单独缓存数据,也可以和SDWebImageDownloader一样单独使用一个单例

添加缓存的方法:

bash 复制代码
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:

bash 复制代码
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];

读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。

bash 复制代码
    SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
    [imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
        // image is not nil if image was found
    }];

四、实现原理

我们在这里给出SDWebImage的架构图与流程图

  1. 架构图(UML 类图)
  2. 流程图(方法调用顺序图)

我们首先通过文字对SDWebImage的方法调用进行分析

  • 首先我们在使用SD时首先调用了[cell.articleImageView sd_setImageWithURL:[NSURL URLWithString:imageURL]];
  • 我们进入这个方法内部,同时来到了UIView+WebCache文件中(这里之所以时UIView是因为UIButton与UIImageView都可以使用SD来进行图片的一系列操作),发现这个方法内部仍然是一个方法

    这些方法最后都会调用同一个方法
    - (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
  • 我们继续进入sd_internalSetImageWithURL:内部去看他的实现操作,这个方法首先保证了图片加载的唯一性,也就是同一个资源不会进行重复的下载,然后进行一系列加载前的配置操作之后正式开始进行图片的加载操作
  • 图片的加载操作我们就进入到了SDWebImageManager这个类中,在这个类中我们创建一个新的操作去用于管理本次加载,同时通过加锁对错误集合进行访问,查询当前URL是否在我们的错误URL集合中,如果失败则立即完成回调,成功则将当前操作加入到操作队列中,紧接着就会拿着图片缓存的key进行查询缓存以及下载操作
  • 查询缓存操作会进入SDImageCache,默认查询磁盘与内存,当然也可以选择查询。如果是默认查询,首先我们会拿着图片缓存的 key (这个 key 默认是图片 URL)去 SDImageCache 单例中读取内存缓存。
  • 如果没有则会根据需要开启同步或是异步线程查询磁盘缓存,如果查询到了磁盘缓存就会将其同步缓存到内存中去,然后再返回给SDWebImageManager
  • 如果两者都没有,SDWebImageManager 就会调用 SDWebImageDownloader 单例的 -downloadImageWithURL: options: progress: completed: 方法去下载,我们首先会检查是否查询到缓存图像或是需要刷新缓存如果是则会检查是否允许从网络下载图像
  • 如果决定下载图像,会调用图像加载器的requestImageWithURL:方法执行下载任务,传递URL、选项、上下文和进度回调。下载完成后还会通过回调处理结果判断是否将URL添加到错误集合中,如果下载没有问题就会执行callTransformProcessForOperation并将图像保存到缓存中
  • 如果有缓存的图像则直接将缓存图像进行回调,就不会执行下载任务了。
  • 无论下载成功、从缓存获取,还是因为不允许下载,最终都会从当前运行的操作列表中安全地移除当前操作
  • 通过重重回调,要回调的数据沿着SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache一路流动,其中流动到SDWebImageManager中时对图片进行了缓存,最后在UIView+WebCache中为UIImageView设置了处理好的图片。

我们接下来将会根据方法调用顺序图对我们的源码进行分析

五、具体实现流程

sd_setImageWithURL

  1. UIImageView+WebCache:
    sd_setImageWithURL方法

    直接进入内部方法sd_internalSetImageWithURL

sd_internalSetImageWithURL

sd_internalSetImageWithURL方法

该方法是整个SDWebImage实现机制中的核心部分,代码如下:

bash 复制代码
# pragma mark 调用入口1
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
                                              placeholderImage:(nullable UIImage *)placeholder
                                                       options:(SDWebImageOptions)options
                                                       context:(nullable SDWebImageContext *)context // 上下文的实质其实是一个字典
                                                 setImageBlock:(nullable SDSetImageBlock)setImageBlock
                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                     completed:(nullable SDInternalCompletionBlock)completedBlock {
    
    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode wont
    // 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.
    // 一个常见的错误就是传入的URL不是NSURL类型而是NSString类型,SDWeb允许传入nsstring类型
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 防止因为不是URL类型而导致崩溃
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    
    if (context) {
        // 创建副本以避免直接修改可变对象
        context = [context copy];
    } else {
        // 如果没有提供上下文则创建一个空的字典作为上下文
        context = [NSDictionary dictionary];
    }
    // 尝试从上下文中获取键值
    // valid 有效
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // 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];
    }
    // 更新最新操作键
    self.sd_latestOperationKey = validOperationKey;
    // 默认情况下,如果没有设置SDWebImageAvoidAutoCancelImage选项,则取消与当前设置图片操作键相关联的所有先前的下载操作。
    if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) { // SDWebImageAvoidAutoCancelImage ------ 不要自动取消之前的下载操作
        // cancel previous loading for the same set-image operation key by default
        [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    }
//    在 UI 开发中,尤其是在列表或滚动视图中,用户可能快速滚动,视图复用机制会导致视图的内容需要频繁更新。如果不取消先前的下载操作,就可能出现以下问题:
//
//    性能问题:同时进行多个不必要的下载任务,增加内存和网络的负担。
//    数据错误:旧的下载任务可能后于新的任务完成,导致视图上显示的图片是错误的。

    
    // 获取或创建与当前操作键关联的图片加载状态对象
    SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey];
    if (!loadState) {
        loadState = [SDWebImageLoadState new];
    }
    // 设置加载对象的url为当前的url
    loadState.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 its 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];
        });
    }
#pragma mark 开始图片加载的操作设置
    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.0
                dispatch_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 state
            // 将进度标记为完成状态
            if (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
            //情况1a:我们得到了一个图像,但SDWebImageAvoidAutoSetImage标志被设置
            //或
            //情况1b:我们没有图像,并且没有设置SDWebImageDelayPlaceholder
            // 根据不同情况处理图片设置
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClosure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            // 如果加载得到了图片并且现在可以直接加载
            if (image) {
                //情况2a:我们得到一个图像和SDWebImageAvoidAutoSetImage没有设置
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) { // 检查options枚举中是否包含SDWebImageDelayPlaceholder选项。
                //情况2b:我们没有图像,并且设置了SDWebImageDelayPlaceholder标志
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // 检查是否应该使用图片过渡效果。
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;
            if (options & SDWebImageForceTransition) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = 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, fallback
                    shouldUseTransition = NO;
                }
            }
            if (finished && shouldUseTransition) {
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_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];
#endif
        if (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.通过validOperationKey取消正在运行的任务

bash 复制代码
    // 尝试从上下文中获取键值
    // valid 有效
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // 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];
    }
    // 更新最新操作键
    self.sd_latestOperationKey = validOperationKey;
    // 默认情况下,如果没有设置SDWebImageAvoidAutoCancelImage选项,则取消与当前设置图片操作键相关联的所有先前的下载操作。
    if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) { // SDWebImageAvoidAutoCancelImage ------ 不要自动取消之前的下载操作
        // cancel previous loading for the same set-image operation key by default
        [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    }
//    在 UI 开发中,尤其是在列表或滚动视图中,用户可能快速滚动,视图复用机制会导致视图的内容需要频繁更新。如果不取消先前的下载操作,就可能出现以下问题:
//
//    性能问题:同时进行多个不必要的下载任务,增加内存和网络的负担。
//    数据错误:旧的下载任务可能后于新的任务完成,导致视图上显示的图片是错误的。

这个方法确保了当前资源仅被下载一次,不会重复下载同一资源,避免内存浪费

2.sd_cancelImageLoadOperationWithKey

bash 复制代码
#pragma mark 定义一个方法,用于取消与特定键(key)相关联的图片加载操作
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // 如果调用者没有提供一个有效的键(key),则使用当前实例的类名作为键。
    if (!key) {
        key = NSStringFromClass(self.class);
    }
    // Cancel in progress downloader from queue
    // 从队列中取消下载任务
    // 从当前对象获取存取所有图片的字典
    SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
    // 获取需要取消的图片对象
    id<SDWebImageOperation> operation;
    
    // 进行加锁, 只有一个线程能取消当前加载进程
    // 因为图片加载操作可能在不同的线程中启动或取消,所以需要保护对共享资源的访问。
    @synchronized (self) {
        operation = [operationDictionary objectForKey:key];
    }
    // 实现cancel方法
    if (operation) {
        if ([operation respondsToSelector:@selector(cancel)]) { // 安全调用可选方法  respondsToSelector 判断当前方法是否安全被调用
            [operation cancel];
        }
        // 使用加锁操作对对应的键进行移除操作
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key];
        }
    }
}

获取操作字典与操作队列同时将操作与对应的key从其中移除

3.初始化SDWebImageManager

bash 复制代码
    // 从上下文中获取图片管理器,没有就创建一个
    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];
    }

4.判断是否需要使用弱缓存,并根据placeholder显示图片

bash 复制代码
    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];
        });
    }

5.判断是否有url同时进入下一阶段

bash 复制代码
if (url) {
	//code in here
}

接下来分析的代码都在上方判断url是否存在的if分支内部

6.进行加载图片前的一系列配置操作比如小菊花等

bash 复制代码
#pragma mark 开始图片加载的操作设置
    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.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            // 调用外部提供的进度回调
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

7.加载内容

其中进入block前调用@weakify(self);进入block后调用@strongify(self);,作用是避免循环引用

当一个 block 捕获了 self 时,它会对 self 产生一个强引用。

如果 self 同时也强引用了这个 block(例如,将这个 block 作为一个属性或者实例变量存储),那么就会产生一个循环引用。

循环引用会导致self与Block都无法正确被释放

bash 复制代码
        // 用弱饮用避免循环引用
        @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 state
            // 将进度标记为完成状态
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }

紧接着我们进入loadImageWithURL

loadImageWithURL

一样的我们给出整个部分的实现流程,然后再逐步分析

bash 复制代码
#pragma mark 定义一个方法来加载图片,接收图片的URL、加载选项、上下文信息、进度回调和完成回调作为参数
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull 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的类型
    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;

    BOOL isFailedUrl = NO;
    if (url) { // 如果url存在,检查它是否在访问失败的URL列表里
        SD_LOCK(_failedURLsLock); // 加锁是为了防止多个线程访问同一个资源,比如这里的self.failedURLs就属于共享资源,为了防止其他线程在当前线程访问时对其进行修改操作所以要加锁
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }
    
    // 预处理选项和上下文参数,确定最终的结果。
    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.创建一个新的操作用于管理这次加载

bash 复制代码
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

2.检查目前的URL是否在先前的失败访问集合中

bash 复制代码
    BOOL isFailedUrl = NO;
    if (url) { // 如果url存在,检查它是否在访问失败的URL列表里
        SD_LOCK(_failedURLsLock); // 加锁是为了防止多个线程访问同一个资源,比如这里的self.failedURLs就属于共享资源,为了防止其他线程在当前线程访问时对其进行修改操作所以要加锁
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }

这里通过递归锁保证了对共享资源的安全访问,共享资源就是self.failedURLs,之所以加锁是为了防止其他线程访问当前集合并对其进行修改

3.如果URL无效或是失败的URL没有设置重试选项, 立即调用完成回调并返回错误信息

bash 复制代码
    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; // 返回操作实例
    }

4.如果上述都没问题就将当前操作加到操作队列中并且预处理最终结果

bash 复制代码
    // 预处理选项和上下文参数,确定最终的结果。
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    // 将当前操作添加到正在运行的操作列表中并且进行加锁保证线程安全
    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);

紧接着我们进入callCacheProcessForOperation中查找缓存

callCacheProcessForOperation

bash 复制代码
// Query normal cache process
// 查询缓存的正常流程
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(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) {
        // 根据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) {
                // 如果操作被取消或是不存在则返回错误
                [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 transform
                if (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.首先在函数中获取查询图像或是查询类型

bash 复制代码
 // 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];
    }

2.同样移除先前不匹配的上下文

bash 复制代码
    if (shouldQueryCache) {
        // 根据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;

3.进入SDWebCache进行缓存查询操作

bash 复制代码
 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);

queryImageForKey

bash 复制代码
- (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 image
            if (image.sd_imageFrameCount > 1) {
#if SD_MAC
                image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
                image = [[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;
    }
    
    // 接下来查询磁盘缓存
    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));
    // 定义从磁盘查询数据的Block
    NSData* (^queryDiskDataBlock)(void) = ^NSData* { // 定义Block,对取消操作进行加锁
        @synchronized (operation) {
            if (operation.isCancelled) {
                return nil;
            }
        }
        // 如果操作没有被取消,从所有可能路径中搜索数据
        return [self diskImageDataBySearchingAllPathsForKey:key];
    };
    // 定义从磁盘创建图像的Block
    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
            // 如果已经在内存中找到图像,但是需要图像数据
            diskImage = image;
        } else if (diskData) {
            BOOL shouldCacheToMomery = YES;
            if (context[SDWebImageContextStoreCacheType]) { // 检查是否应该将图像缓存到内存中
                SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
                shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
            }
            // 特殊情况:如果用户查询同一URL的图像以避免多次解码和写入相同的图像对象到磁盘缓存中,我们在这里再次查询和检查内存缓存
            if (shouldCacheToMomery && self.config.shouldCacheImagesInMemory) {
                diskImage = [self.memoryCache objectForKey:key];
            }
            // 如果内存缓存未命中,解码磁盘数据
            if (!diskImage) {
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                // check if we need sync logic
                if (shouldCacheToMomery) {
                    [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.首先检查缓存键,为无或是为空都直接返回nil

bash 复制代码
 if (!key) { // 如果缓存键为空,则立即完成回调
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    // Invalid cache type
    // 如果缓存类型为无也立即完成回调
    if (queryCacheType == SDImageCacheTypeNone) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

2.接着首先检查内存缓存,如果找到了图像则保存第一帧

bash 复制代码
// 首先检查内存缓存
    UIImage *image;
    // 如果查询类型没有要查询磁盘, 则直接只查询内存
    if (queryCacheType != SDImageCacheTypeDisk) {
        image = [self imageFromMemoryCacheForKey:key];
    }
    // 如果找到了图像
    if (image) {
        // 只解码第一帧保证图片是静态的
        if (options & SDImageCacheDecodeFirstFrameOnly) {
            // Ensure static image
            if (image.sd_imageFrameCount > 1) {
#if SD_MAC
                image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
                image = [[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;
            }
        }
    }

3.检查是否只需要查询内存,只查询内存的话之后立即回调,不再查询磁盘

bash 复制代码
    BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

4.接下来选择异步还是同步查询磁盘缓存

bash 复制代码
 BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
    // 定义从磁盘查询数据的Block
    NSData* (^queryDiskDataBlock)(void) = ^NSData* { // 定义Block,对取消操作进行加锁
        @synchronized (operation) {
            if (operation.isCancelled) {
                return nil;
            }
        }
        // 如果操作没有被取消,从所有可能路径中搜索数据
        return [self diskImageDataBySearchingAllPathsForKey:key];
    };
    // 定义从磁盘创建图像的Block
    UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
        @synchronized (operation) {
            if (operation.isCancelled) {
                return nil;
            }
        }

5.判断是否将查询到的磁盘数据缓存到内存缓存中

bash 复制代码
            // 如果内存缓存未命中,解码磁盘数据
            if (!diskImage) {
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                // check if we need sync logic
                if (shouldCacheToMomery) {
                    [self _syncDiskToMemoryWithImage:diskImage forKey:key]; // 将数据同步缓存到内存中
                }
            }

6.如果缓存中图片不存在,再去查询原始缓存

缓存与原始缓存的区别是一个图像是否经过了变换

bash 复制代码
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 transform
                if (mayInOriginalCache) { // 可能存在在原始缓存中,就用原始缓存查询流程
                    [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                    return;
                }

如果内存缓存与磁盘缓存中都没有找到那么就启用下载流程,如果找到了就直接回调,后面callDownloadProcessForOperation方法会判断是否需要下载

callDownloadProcessForOperation

bash 复制代码
// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(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.首先标记缓存操作结束

bash 复制代码
    // Mark the cache operation end
    // 标记缓存操作结束
    @synchronized (operation) {
        operation.cacheOperation = nil;
    }

2.如果需要下载就发起下载请求

bash 复制代码
 operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {

3.下载失败便将url加到错误集合中

bash 复制代码
            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);
                }

4.进行图像转换流程并进行图像存储操作

bash 复制代码
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];

这里不对图像转换进行相关描述,着重于保存操作

storeImage

1.将图像存储到内存缓存中

bash 复制代码
if (image && toMemory && self.config.shouldCacheImagesInMemory) {
    NSUInteger cost = image.sd_memoryCost;
    [self.memoryCache setObject:image forKey:key cost:cost];
}

2.决定是否继续存储到磁盘

如果不需要存储到磁盘,则调用完成回调并返回。

bash 复制代码
if (!toDisk) {
    if (completionBlock) {
        completionBlock();
    }
    return;
}

3.将数据存储到磁盘中

bash 复制代码
            dispatch_async(self.ioQueue, ^{
                [self _storeImageDataToDisk:encodedData forKey:key];
                [self _archivedDataWithImage:image forKey:key];
                if (completionBlock) {
                    [(queue ?: SDCallbackQueue.mainQueue) async:^{
                        completionBlock();
                    }];
                }
            });
        });
    } else {
        dispatch_async(self.ioQueue, ^{
            [self _storeImageDataToDisk:data forKey:key];
            [self _archivedDataWithImage:image forKey:key];
            if (completionBlock) {
                [(queue ?: SDCallbackQueue.mainQueue) async:^{
                    completionBlock();
                }];
            }
        });

setImage

下载成功后,通过重重回调,要回调的数据沿着SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache一路流动,其中流动到SDWebImageManager中时对图片进行了缓存,最后在UIView+WebCache中为UIImageView设置了处理好的图片。

这里我们重新回到sd_internalSetImageWithURL方法中,更新一系列外部配置像图片过度效果等后,在主线程调用sd_setImage更新UI

bash 复制代码
if (setImageBlock) {
        finalSetImageBlock = setImageBlock;
    } else if ([view isKindOfClass:[UIImageView class]]) {
        UIImageView *imageView = (UIImageView *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            imageView.image = setImage;
        };
    }
#if SD_UIKIT
    else if ([view isKindOfClass:[UIButton class]]) {
        UIButton *button = (UIButton *)view;
        finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
            [button setImage:setImage forState:UIControlStateNormal];
        };
    }

通过判断类是button还是imageview来设置不同设置方法

总结

由此我们总结一下SDWebImage的调用流程

首先我们会进入setImagewithURL:方法中,然后进入sd-InternalmageWithURL方法中,在这个方法中我们首先通过validOperationKey取消正在运行的任务,任务是通过sd_cancelImageLoadOperationWithKey方法取消的,这一步是为了避免同一资源被重复下载,接着我们初始化SDWebManager(这里因为SDWebManager是单例,所以只初始化一次),接着进行一系列配置后调用loadImageWithURL方法,首先检查URL是否在错误的集合中,如果没有就调用queryImageForKey去查找缓存,查找缓存的步骤是首先查找内存缓存,内存缓存找不到再去查找磁盘缓存,都找不到则去查询原始数据。如果都找不到我们就去执行下载操作,下载操作完成后通过storeImage方法将图像存储到缓存中,最后回到SDWebImageManager单例类中通过setImage方法将Image设置在对应的视图上

相关推荐
goodSleep7 分钟前
更新Mac OS Tahoe26用命令恢复 Mac 启动台时不小心禁用了聚焦搜索
macos
使一颗心免于哀伤7 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
叽哥7 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸2 天前
macOS自带截图命令ScreenCapture
macos
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
侃侃_天下2 天前
最终的信号类
开发语言·c++·算法