SDWebImage 源码学习笔记 ☞ SDWebImageManager

SDWebImage-源码学习笔记.png

前言

这是本系列的第 3 篇,在前一篇中,我们了解了 SDWebImage 执行的基本流程,本篇就来介绍第一个核心类 SDWebImageMananger

正文

SDWebImageMananger.h 文件基本可以分为 3 各部分:

①定义了一个枚举 SDWebImageOptions,列举了可能会用到的一些场景。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    // 重试已经失败的 url
    SDWebImageRetryFailed = 1 << 0,
    // 低优先级,比如,在有 UI 交互的情况下,会延迟下载操作
    SDWebImageLowPriority = 1 << 1,
    // 下载完成后,仅做内存缓存,不做磁盘缓存
    SDWebImageCacheMemoryOnly = 1 << 2,
    // 下载过程中逐步加载图片,而不是完全下载完之后才展示
    SDWebImageProgressiveDownload = 1 << 3,
    // 刷新缓存
    SDWebImageRefreshCached = 1 << 4,
    // 当 App 进入后台时,继续下载任务,如果后台任务超时,操作将被自动取消
    SDWebImageContinueInBackground = 1 << 5,
    // 允许处理 Cookie
    SDWebImageHandleCookies = 1 << 6,
    // 允许不受信任的 SSL 证书,生产环境慎用
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    // 高优先级,即会把相应的图片放到最前边加载,而不是按照加入队列时的顺序执行
    SDWebImageHighPriority = 1 << 8,
    // 延迟 placeholder 的加载,即在下载完成时才加载
    SDWebImageDelayPlaceholder = 1 << 9,
    // 对动图也执行 transform 操作
    SDWebImageTransformAnimatedImage = 1 << 10,
    // 图片下载完成后,不直接自动给 imageView 赋值,给用户调整图片的机会
    SDWebImageAvoidAutoSetImage = 1 << 11,
    // 依据设备内存缩放图片,如果设置了 `SDWebImageProgressiveDownload` ,此设置无效
    SDWebImageScaleDownLargeImages = 1 << 12,
    // 在有内存缓存的情况下,依然需要查询磁盘缓存,建议与 SDWebImageQueryDiskSync 配合使用
    SDWebImageQueryDataWhenInMemory = 1 << 13,
    // 同步查询磁盘缓存
    SDWebImageQueryDiskSync = 1 << 14,
    // 仅加载缓存图片
    SDWebImageFromCacheOnly = 1 << 15,
    // 对内存和磁盘中的 image 也执行 transition 的操作
    SDWebImageForceTransition = 1 << 16
};

②定义了一个协议 SDWebImageManagerDelegate,这里提供了以下 3 个协议方法:

// 缓存中没有指定图片时,是否需要下载
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

// 是否需要将制定 URL 标记为失败的 URL
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;

// 允许在刚刚下载到 image 并且未做缓存之前,对图片执行 transform,返回处理后的 image
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

③SDWebImageManager 的头文件,有几个重要属性,他们的作用见下边的注释。

// 代理对象
@property (weak, nonatomic, nullable) id  delegate;
// 处理缓存的对象
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
// 处理下载工作的对象
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
// 一个用户定义的 block,用于生成 cacheKey
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// 一个用户定义的 block,用于序列化下载到的数据
@property (nonatomic, copy, nullable) SDWebImageCacheSerializerBlock cacheSerializer;

下面是 2 个常用的创建方法:+ (nonnull instancetype)sharedManager;- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader;,其实最终都是调用了后者 。

// 单例
+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

// 初始化,创建处理缓存和下载任务的对象 cache 和 downloader
- (nonnull instancetype)init {
    SDImageCache *cache = [SDImageCache sharedImageCache];
    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    return [self initWithCache:cache downloader:downloader];
}

// 核心的初始化方法,为各属性赋初值
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        // 处理缓存和下载任务的对象
        _imageCache = cache;
        _imageDownloader = downloader;
        // 用于存储请求失败的 URL 的集合及操作时用的锁 (信号量)
        _failedURLs = [NSMutableSet new];
        _failedURLsLock = dispatch_semaphore_create(1);
        // 存储运行中 operation 的集合,通过判断他的 count 是否为 0,判断操作是否在进行中:BOOL isRunning = (self.runningOperations.count > 0);
        _runningOperations = [NSMutableSet new];
        // 操作时用的锁 (信号量)
        _runningOperationsLock = dispatch_semaphore_create(1);
    }
    return self;
}

另外几个方法,就不单独介绍了,用到的时候再继续讨论。此处,我们只看一个核心方法 - (nullable id )loadImageWithURL:url options:options progress:progressBlock completed:completedBlock;,下面我们来一步步讨论这个方法的具体实现。

1.校验参数

依次做如下处理:如果传入的 completedBlock 为空,就直接报错;如果传入的参数是 NSString * 类型的,需要将其转换成 NSURL;最后,如果 url 还不是 NSURL 类型,那就只能将其置为 nil,以免造成后边 Crash。

    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
2.生成总的 operation

他是 SDWebImageCombinedOperation 实例对象,也是当前方法要返回的结果,并将当前类赋值给 operation 的一个 weak 属性(避免循环引用)。

SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 属性

SDWebImageCombinedOperation 的声明与实现文件均在当前类 SDWebImageManager 的实现文件里边,简单看一下他的 .h/.m 文件吧。

// SDWebImageCombinedOperation.h
@interface SDWebImageCombinedOperation : NSObject 
// 标识是否已取消
@property (assign, nonatomic, getter = isCanceled) BOOL cancelled;
// downloadToken 这是一个继承自 NSObject 的类,他有一个继承自 NSOperation 的属性,也就是真正执行下载操作时的 operation,cancel 时会用到
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
// 查询缓存时的 operation,用于标识当前 operation 是否已经被取消。其实查询缓存时,首先查看 operation.isCanceled,如果没被取消了,就会再去查询了。cancel 时会将其 isCanceled 属性置为 YES。
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
// manager
@property (weak, nonatomic, nullable) SDWebImageManager *manager;

@end

// SDWebImageCombinedOperation.m
#pragma mark - 代理方法实现

@implementation SDWebImageCombinedOperation

- (void)cancel {
    @synchronized(self) {
        self.cancelled = YES;
        // 取消查询缓存的 Operation,此时 isCanceled 会被置为 YES。
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        // 取消下载操作
        if (self.downloadToken) {
            [self.manager.imageDownloader cancel:self.downloadToken];
        }
        // 将当前 operation 从 manager 中运行着的 operation 数组中移除。
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

@end

//  SDWebImageOperation 协议的定义
@protocol SDWebImageOperation 
- (void)cancel;
@end

可以看到,SDWebImageCombinedOperation 这个类的主要作用就是 cancel 操作,包括 cancel 查询缓存 和 cancel 下载数据。

3.再次检测一下 url

如果是曾经失败的 url,而且不允许重试,或者 url 为空时,执行 completionBlock,并返回当前 operation。

// self.failedURLs 是一个保存曾经失败过的 URL 的数组,用于检测当前 URL 是不是曾经请求失败过的URL.另外,搜索一个个元素的时候,NSSet 比 NSArray 查询更快。
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    // 若出现以下两种情况就不再往下走了,直接执行 CompletionBlock:① URL 是空的;② 此 URL 是曾经请求失败的 URL,并且规定不允许重新请求曾经失败的 URL。
    if (url.absoluteString.length == 0
        || (!(options & SDWebImageRetryFailed) && isFailedUrl))
    {
        [self callCompletionBlockForOperation:operation
                                   completion:completedBlock
                                        error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]
                                          url:url];
        return operation;
    }
4.保存 operationself.runningOperations

后者是一个数组,这里使用了信号量来确保线程安装。

    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
5.查询缓存。
    NSString *key = [self cacheKeyForURL:url];
    
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
                                                                  options:cacheOptions
                                                                     done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
    {
        // 查询完成后的操作在这里,可能查到了,也可能没查到...
    }

这里先准备了 2 个参数,查询的依据 key 和一些条件 cacheOptions。key 的获取是通过一个私有方法 (如下),如果自定义了 key 的生成规则 self.cacheKeyFilter,就用自定义的,如果没有,就直接取 url.absoluteString。cacheOptions 是一个用 NS_OPTIONS 定义的枚举类型 (前边已介绍过),可组合多种情况,在这里综合了 2 个查询的要求和 1 个缩放图片的要求。

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}
6.查询的具体过程

详情将会在 SDImageCache 中介绍,下面讨论一下查询缓存结束后的操作。

7.移除当前 operation

从 self.runningOperations 这个数组中移除当前 operation。

if (!strongOperation || strongOperation.isCancelled) {
    [self safelyRemoveOperationFromRunning:strongOperation];
    return;
}
8.判断是否需要下载

当同时满足 3 个要求时,就需要下载新数据了:
①没要求只能从缓存获取数据,即当缓存找不到时,可以去下载;
②找不到缓存 或 要求必须更新缓存;
③当 self.delegate 没有遵守协议, 或者 协议方法返回 YES。

BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly)) 
        && (!cachedImage || options & SDWebImageRefreshCached) 
        && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
9.若需要下载
  • 首先依然要做一个判断,即 如果有缓存数据并且要求刷新缓存数据时,需要先调用一次 CompletionBlock,将缓存数据返回去,然后再开始下载新数据,代码如下:
if (cachedImage && options & SDWebImageRefreshCached) {
    [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}

然后,准备下载数据时所需的一些基本选项,可以参考篇头介绍的枚举 SDWebImageOptions

  • 开始下载,调用了 SDWebImageDownloader 的下载方法,留待 SDWebImageDownloader 介绍,这里只讨论下载完成之后的操作。
__weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
                                                                               options:downloaderOptions
                                                                              progress:progressBlock
                                                                             completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
        // 下载完成后的操作...
}

下载完成后,可以分这么几种情况:
a.当前 operation 已经被取消,这种情况下不作任何操作,包括回调。

b.下载出错,先将失败的 error 信息返回,然后决定是否需要将当前 URL 存入失败的 URL 数组。

c.下载成功,此时要做的工作还有许多:

①如果设置了失败重发,则将当前 URL 从失败的 URL 数组中移除。

if ((options & SDWebImageRetryFailed)) {
    LOCK(self.failedURLsLock);
    [self.failedURLs removeObject:url];
    UNLOCK(self.failedURLsLock);
}

②对于自定义的 manager,需要执行另外一套缩放标准。

if (self != [SDWebImageManager sharedManager]
                        && self.cacheKeyFilter
                        && downloadedImage)
{
    downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}

③若需要更新缓存,但是未下载到图片,且缓存中本来有值的情况下,什么也不做,因为下载之前早已经缓存数据返回了。

if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// 需要更新缓存,但是未下载到图片,且缓存中本来有值的情况下,什么也不做,因为下载之前早已经缓存数据返回了
}

④如果下载到了图片,并且要求 transform 图片的情况下,异步执行 transform 和缓存图片的工作,然后回到主线程执行 completionBlock。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        UIImage *transformedImage = [self.delegate imageManager:self
                                       transformDownloadedImage:downloadedImage
                                                        withURL:url];
        
        if (transformedImage && finished) {
            
            BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
            NSData *cacheData;
            // pass nil if the image was transformed, so we can recalculate the data from the image
            if (self.cacheSerializer) {
                cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
            } else {
                cacheData = (imageWasTransformed ? nil : downloadedData);
            }
            
            // *** 存盘:注意是存的 imageData
            [self.imageCache storeImage:transformedImage
                              imageData:cacheData
                                 forKey:key
                                 toDisk:cacheOnDisk
                             completion:nil];
        }
        
        [self callCompletionBlockForOperation:strongSubOperation
                                   completion:completedBlock
                                        image:transformedImage
                                         data:downloadedData
                                        error:nil
                                    cacheType:SDImageCacheTypeNone
                                     finished:finished
                                          url:url];
    });

⑤如果下载到了图片,并且下载完成的话,则存盘并执行 completionBlock。存盘调用了 SDImageCache 的方法,随后介绍。

最后将当前 operation 移除。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        UIImage *transformedImage = [self.delegate imageManager:self
                                       transformDownloadedImage:downloadedImage
                                                        withURL:url];
        
        if (transformedImage && finished) {
            
            BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
            NSData *cacheData;
            // pass nil if the image was transformed, so we can recalculate the data from the image
            if (self.cacheSerializer) {
                cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
            } else {
                cacheData = (imageWasTransformed ? nil : downloadedData);
            }
            
            // *** 存盘:注意是存的 imageData
            [self.imageCache storeImage:transformedImage
                              imageData:cacheData
                                 forKey:key
                                 toDisk:cacheOnDisk
                             completion:nil];
        }
        
        [self callCompletionBlockForOperation:strongSubOperation
                                   completion:completedBlock
                                        image:transformedImage
                                         data:downloadedData
                                        error:nil
                                    cacheType:SDImageCacheTypeNone
                                     finished:finished
                                          url:url];
    });

// ...

if (finished) {
    [self safelyRemoveOperationFromRunning:strongSubOperation];
}
10.若不需要下载,并且有缓存

此时,执行 completionBlock 将缓存数据返回,然后移除当前 operation。

[self callCompletionBlockForOperation:strongOperation
                           completion:completedBlock
                                image:cachedImage
                                 data:cachedData
                                error:nil
                            cacheType:cacheType
                             finished:YES
                                  url:url];
            
[self safelyRemoveOperationFromRunning:strongOperation];
11.其它,即没有缓存,且不需要下载

和上边的操作类似,只不过传回的 image 和 data 均为 nil。

    [self callCompletionBlockForOperation:strongOperation
                               completion:completedBlock
                                    image:nil
                                     data:nil
                                    error:nil
                                cacheType:SDImageCacheTypeNone
                                 finished:YES
                                      url:url];
    
    [self safelyRemoveOperationFromRunning:strongOperation];

最后将 operation 返回。

小结

以上就是 SDWebImageManager 这个类的主要功能,其中关于缓存和下载的内容,详见后边几篇的讨论。

你可能感兴趣的:(SDWebImage 源码学习笔记 ☞ SDWebImageManager)