美文网首页源码解读
SDWebImage源码学习笔记 ☞ SDWebImageMan

SDWebImage源码学习笔记 ☞ SDWebImageMan

作者: RiverSea | 来源:发表于2018-12-09 23:33 被阅读17次
    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 <SDWebImageManagerDelegate> 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];isRunning
            // 操作时用的锁 (信号量)
            _runningOperationsLock = dispatch_semaphore_create(1);
        }
        return self;
    }
    

    另外几个方法,就不单独介绍了,用到的时候再继续讨论。此处,我们只看一个核心方法 - (nullable id <SDWebImageOperation>)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 <SDWebImageOperation>
    // 标识是否已取消
    @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 <NSObject>
    - (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源码学习笔记 ☞ SDWebImageMan

        本文链接:https://www.haomeiwen.com/subject/ecqocqtx.html