SDWebImage(v4.4.2)源码学习及知识点分析

SDWebImage这个第三方库有多厉害,从它的GitHub上过万的Star就可以看出来。一直以来都想好好拜读它的源码,但之前每次都看得头昏脑胀的,最后都是不了了之。方知武侠小说中修为没到,强练绝世秘籍会导致走火入魔的说法并不是无稽之谈。

害怕.jpg

最近项目没有这么紧张,又静下心来,好好研读了几遍。终于看出了一点点门道,所以写篇笔记记录一下。话不多说,进入正题。先来一张流程图压压惊:

流程图.png

本文采用讲解主体逻辑,贴出源码,并在源码中添加注释的方法;同时会把比较有特色的点,结合自己的理解,稍作分析。作为iOS码农界的小学生,能力有限,水平一般(脑补郭德纲的声音。。。)。如有不对之处,还望指正。


为已有的类添加方法,毫无疑问应该首先想到类别(Category)这种方法。那直接进入到UIImageView+WebCache.m文件中,看到一系列的方法,其实最终都是走到了UIView+WebCache.m的的这个方法中:

- (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
                           context:(nullable NSDictionary<NSString *, id> *)context;

为什么要这么设计呢?因为前面流程图上说了,不光UIImageView有扩展,UIButton也有扩展方法,那么把最终的实现放到他们的共同父类UIView的类别中,也就顺理成章了。方法实现中,一进来是这么两行代码:

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

这个validOperationKey其实就是为了缓存或者从缓存中查找operation当作key用的;如果外部没有传入的话,就默认的取类名。先用这个key取消可能正在操作的operation,避免后续的回调混乱,保证这个Imageview或者Button只存在一个请求图片的操作。进入sd_cancelImageLoadOperationWithKey:方法的内部,可以看到是在UIView+WebCacheOperation.m中实现的。这里将key和operation映射保存在动态绑定的SDOperationsDictionary中,名字是dictionary,实际上用到的是NSMapTable。类似于字典,但比字典更灵活的一个类。这篇文章说的比较详细。
接下来是动态绑定url到当前对象上;如果设置的options不是延迟设置占位图的话,就在主线程回调设置占位图:

//动态绑定url到当前对象上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果设置的options不是延迟设置占位图的话,就在主线程回调设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

接下来的这个最外层的if else的大逻辑就是,如果传入的url不为nil的话,就走下面一大段逻辑;否则发起错误回调。看看url不为空后续的逻辑,对照着注释应该比较清楚:

#if SD_UIKIT
        // 检查activityView是否可用
        if ([self sd_showActivityIndicatorView]) {
            //添加菊花控件
            [self sd_addActivityIndicator];
        }
#endif
        
        // 初始化sd_imageProgress的总任务数和任务完成数
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        //取到manager,如果外部传入了就用传入的值
        SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        //弱引用self,防止引用循环
        __weak __typeof(self)wself = self;
        //把传进来的progressBlock封装一下,后续生成operation时使用
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

其中的SD_UIKIT宏定义后面会频繁出现,其实就类似一个bool值,在iOS和tvOS中为真,其他系统下为假:

#if TARGET_OS_IOS || TARGET_OS_TV
    #define SD_UIKIT 1
#else
    #define SD_UIKIT 0
#endif

然后是生成operation的实现:

id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
#if SD_UIKIT
            [sself sd_removeActivityIndicator];
#endif
...

代码太长了没有截完,大体逻辑就是manager使用传进来的url,options,completedBlock,和上面生成的combinedProgressBlock参数,生成一个operation,并把它和key映射保存在动态绑定的SDOperationsDictionary中。我们不管生成operation的细节,先来看看它的回调block实现。

block一进来是对weakSelf的强引用,因为前面对self进行了弱引用。这里说个比较有意思的点。以前最开始看到block实现中这种先weak再strong的做法我其实非常不理解,疑惑的是这样做到底会不会增加该对象的引用计数?如果会的话这跟不做转换有什么区别?当时百度google了一大堆,硬是没有看懂,所以这个问题拖了很久,面试还被问到过。后来有一次突然看到一篇文章里面说,block里面的strong引用weakSelf,是为了防止多线程切换的时候,weakSelf被提前释放了,后续再访问该对象的时候,引起野指针崩溃才这么做的。我突然豁然开朗,原来先weak后strong的目的其实有两个:

  • 先weak引用当前对象,是为了让block捕获的对象是一个弱引用的对象。这样就打破了引用循环,防止双方都强引用对方,形成引用循环,导致内存泄漏。
  • block内部的strong是为了增加对象的引用计数,保证该对象在block内部是一直存在的,防止在多线程切换的时候对象被提前释放,后续访问导致野指针崩溃。

有时候不得不感叹,能够在特定的时间节点碰到对的人或物,是多么幸运的一件事情。

好了,我们继续来看回调block的实现细节,主要是一些条件判断和回调处理,没有什么值得特别说明的,对照注释应该是挺好理解的:

#if SD_UIKIT
            //如果前面添加了菊花控件,这里先移除
            [sself sd_removeActivityIndicator];
#endif
            // 如果操作完成,没有错误,而且progress没有更新的话,手动将其置为Unknown状态。
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            //如果已经完成,或者options设置了不自动赋值图片选项的话,就需要执行完成回调
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //如果(图片存在 且 options设置了不自动赋值图片这个选项的话) 或者 (图片不存在 且 options没有设置延迟显示placeholder图片选项)的话,就不要将回调中的image设置给当前控件。
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            //为callCompletedBlockClojure赋值
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // 检查是否需要执行图片转换
            SDWebImageTransition *transition = nil;
            //如果已经完成 且 (options设置了强制转换选项 或者 缓存类型为SDImageCacheTypeNone,即没有命中缓存,是从网络获取的图片)的话,就取UIView+WebCache.h头文件中定义的sd_imageTransition转换策略。其实是提供了自定义图片转换的功能,SD默认这个属性是nil。
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = sself.sd_imageTransition;
            }
#endif
            //不同宏定义下执行响应的设置方法
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
                callCompletedBlockClojure();
            });

说完了block回调,我们进入manager生成operation的方法中一探究竟,即这个方法:

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock 

这个方法的实现代码比较多,我们先把它拆分成以下几个点来逐一研究:

  • url参数判断转换
  • 判断缓存策略,生成operation的缓存查询操作
  • 在operation的缓存查询回调中看是否需要下载

看一下第一部分,我在代码中加了注释,也是比较好理解的:

    //先是一个断言,指明完成回调是必要的参数,否则调用这个方法是没有意义的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // 防止常见的把NSString当作NSURL传入的错误
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    //operation引用了manager,方便它内部使用。注意operation的manager属性是弱引用,防止引用循环。
    operation.manager = self;

    //查询是否是之前请求过的但是失败了的url,这里用了信号量做锁,下面会详细说一下
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    //如果url长度为空 或者 (options没有设置失败后重试选项 且 是之前请求失败的url) 的话,调用完成回调,回传error。callCompletionBlockForOperation:方法其实没做什么事,最终使用的是一个安全调用block的宏。
    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;
    }

这里比较有意思的是,查询是否是之前请求过的失败url时,使用了信号量加锁。纵观整个SD,很多地方都使用了这种方法替代互斥锁来保证线程安全。把信号量当做锁其实用法也比较简单,看manager 的 init 方法里,初始化了两个信号量,都是当做锁来用的。

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        _failedURLs = [NSMutableSet new];
        _failedURLsLock = dispatch_semaphore_create(1);
        _runningOperations = [NSMutableSet new];
        _runningOperationsLock = dispatch_semaphore_create(1);
    }
    return self;
}
//再看LOCK 和 UNLOCK是什么
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

就是初始化一个为1的dispatch_semaphore_t变量,当前operation抢占到资源时,先调用dispatch_semaphore_wait方法将信号量减1,此时dispatch_semaphore_t变量为零,如果再有其他operation想要获取该变量,就只能排队等着,啥时候前一个operation跑完了dispatch_semaphore_signal方法,将信号量加了1。后面的operation才能获取到该信号量进行下一步。当然信号量是一种比较底层的同步机制,不光是当锁用这么简单。这篇文章有各种锁的说明和比较。

接下来是判断缓存策略,生成operation的缓存查询操作:

    //先将当前operation添加到正在进行的所有operation的无序集合中,也用到了前面说的信号量加锁
    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
    //生成后续查询和存储的key,如果用户自定义了生成key的方法,SD就使用用户自定义的,否则默认为url的absoluteString
    NSString *key = [self cacheKeyForURL:url];
    //获取缓存options
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    //弱引用当前operation
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        //如果当前operation不存在或者已经被取消,则将其从正在进行的operations无序集合中安全的移除,也用到信号量加锁
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // 如果 (options没有设置只从缓存中获取图片选项) 且 (没有命中缓存 或者 options设置了刷新缓存选项) 且 (manager的delegate没有实现imageManager:shouldDownloadImageForURL代理方法 或者 实现了该方法,返回YES) 的话,就需要下载图片
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
            && (!cachedImage || options & SDWebImageRefreshCached)
            && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        if (shouldDownload) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                // 如果命中了缓存,且options设置了刷新缓存选项,那么先执行完成回调,再去请求图片,以刷新NSURLCache的缓存
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // 设置后续downloadToken的options
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            //设置operation的downloadToken
            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {   
            xxx(代码没截完)

这段实现的大体逻辑就是,生成operation以后,设置它的缓存查询,在缓存查询的回调中检查是否需要下载图片,如果需要的话,再设置它的downloadToken。也就是先查询缓存,如果没有命中或者设置了刷新缓存选项的话,就去下载图片。那么我们SDImageCache这个工具类中,缓存查询方法是怎么实现的,对照着注释看应该是比较清楚了:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 先从内存缓存中查询图片,即从SDImageCache的SDMemoryCache类型的memCache属性中查询,SDMemoryCache继承自NSCache,我记得比较早的SD版本,memCache使用的是NSDictionary。NSCache对比字典有什么优势呢?主要有两点,一是NSCache是线程安全的,另一个是NSCache在内存紧张时,会自动清理部分无用数据。
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //如果内存中命中了图片,且options没有设置内存中有数据仍旧查询磁盘缓存的选项的话,就直接执行完成回调,并且返回nil。
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //如果需要进入磁盘查询,先设置好它的回调block
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            if (image) {
                // 图片是从内存缓存中命中的
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                // 图片是从磁盘缓存中命中的
                diskImage = [self diskImageForKey:key data:diskData options:options];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    //如果SDImageCache的config设置了shouldCacheImagesInMemory属性,那么将从磁盘命中的图片保存到内存中,方便下次使用。SD默认将该属性置为YES
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            //如果options设置的是同步查询,就直接执行完成回调;否则,将回调异步提交到主队列。
            if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        //如果options设置的是同步查询,就直接执行queryDiskBlock
        queryDiskBlock();
    } else {
        //否则,将block提交到自己的IO队列,SDImageCache初始化时将该队列指定为了串行,只能一个接一个的执行回调
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

可以看到SD使用的是内存和磁盘的二级缓存,先查询内存,如果命中就直接返回,没有命中的话再查询磁盘缓存;如果磁盘缓存命中,默认会将图片设置到内存缓存中,方便下次使用。同时回调有同步和异步两种选择。缓存查询这块的大体逻辑已经讲完了,我们顺便来看看SDImageCache类中,关于缓存清理这部分的实现逻辑。它在初始化的时候就注册了App将要销毁和进入后台的通知,接到通知以后会自动清理内存,调用删除文件的方法:

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

        // 确定文件的查询键,是AccessDate 还是ModificationDate
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        //查询三个信息,是否是文件夹,存入缓存的时间,文件大小
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];

        // 枚举当前路径下的所有文件
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        //过期时间,SD默认的文件过期时间是一个星期,如果想自定义的话,可以在SDImageCacheConfig中修改
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 这里的for循环有两个目的
        //  1. 将每个过期文件的URL添加到urlsToDelete数组中,后续统一移除对应的文件
        //  2. 将每个文件的信息跟URL对应,存到cacheFiles字典中,后面会用到
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        //移除对应的文件
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果用户设置了最大磁盘缓存尺寸,且当前缓存尺寸超过了设置的最大值。注意SD默认是没有设置最大磁盘缓存的。
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // 设置此次的目标尺寸为最大尺寸的一半
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // 根据文件修改时间将其排序,最老的文件在最前面,也就是说最先删除
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            for (NSURL *fileURL in sortedFiles) {
                if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

所以每次App进入后台,SD都会检查,如果磁盘中存在过期文件则删除;同时如果用户设置了最大磁盘缓存尺寸,且已经使用的磁盘大小超过了这个阈值,会以最大值的一半作为此次清理的目标,从最老的文件开始删,直到达到目标尺寸。
说完缓存查询这部分,我们回到operation生成downloadToken这里。其实调用的是SDWebImageDownloader这个工具类来生成downloadToken:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;

该方法中最主要的是实现了名叫createCallback的回调block,因为其实一个URL对应一个下载操作,如果多个控件使用了同一个URL下载,是没有必要下载多次的。所以SD会在addProgressCallback:completedBlock:forURL:createCallback:方法中判断是否已经存在了当前URL对应的下载操作,不存在的话再调用createCallback创建。创建下载操作其实就是按部就班的设置超时时间(SD默认15秒)、根据缓存策略生成request、设置request的头信息;然后根据request创建对应的SDWebImageDownloaderOperation,然后把SDWebImageDownloader这个工具类的证书验证及操作优先级等属性赋值给它生成的每个SDWebImageDownloaderOperation。在方法的最后,有这样一个if判断:

if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // 用户可以设置下载操作的执行顺序,如果设置了LIFO(Last In First Out)的话,会将前一个下载操作依赖当前下载操作,保证了最后生成的下载操作会最先执行
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

这里稍微引申一下,iOS中最常用的多线程编程方法应该就是NSOperation和GCD了吧,这里可以看到NSOperation对比GCD的一个优点:添加依赖非常方便。当然还有另外的优点比如提交的操作可以取消,可以设置操作的优先级等。所以要根据不同的应用场景选择最合适的工具。我们接着看addProgressCallback:completedBlock:forURL:createCallback:方法:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 因为URL后续会当做保存下载操作的字典查询的key,所以必须保证不为空。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    //同步查询字典中是否存在该url对应的操作
    LOCK(self.operationsLock);
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    //如果不存在 或者 操作已经标记为完成
    if (!operation || operation.isFinished) {
        operation = createCallback();
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            //operation的完成回调中会将自己从URLOperations字典中移除
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];

        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);
    //将progressBlock和completeBlock赋值给cancelToken
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    //生成downloadToken
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

可以看到,从最外层传入的progressBlock 和 completeBlock 最终都赋值给了cancelToken,我们进入该方法看看:

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    LOCK(self.callbacksLock);
    [self.callbackBlocks addObject:callbacks];
    UNLOCK(self.callbacksLock);
    return callbacks;
}

就是将两个回调先用字典装起来,然后添加到callbackBlocks数组中。这里我刚开始看的时候有个疑问,不是一个url对应一个下载吗,为什么SDWebImageDownloaderOperation内部的回调会是个数组呢?后来才想明白,就跟上面说的一样,如果多个控件短时间内加载同一个url,先加载的那个控件生成了一个下载操作,后续就没必要再生成下载操作了,但是回调是必须区分的,因为每个控件的完成回调中,会把图片赋值给当前控件。所以内部的回调要用一个数组来装载,图片下载完成以后依次调用每个回调。

最终的下载操作都是SDWebImageDownloaderOperation这个类实现的。我们知道使用NSOperation实现多线程的话,只有两种方法,一是使用它的子类:NSInvocationOperation 或者 NSBlockOperation;另外就是自定义一个类,继承自NSOperation,覆写它的start方法。可以看到SD使用的是后面一个方法。我们看看它的start方法里都做了些什么:

- (void)start {
     //一般都是在start方法开始的时候就检测当前操作是否被取消。
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

//iOS和tvOS都可以在App进入后台后向系统申请额外的操作时间
#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            //如果外部没有赋值session给它,那么就自己在内部生成一个,并赋值给ownedSession,
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // SD特别指明了 URLCache 的 cachedResponseForRequest:方法不是线程安全的
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            //设定dataTask的优先级
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        //调用progress回调
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        //回调主线程发送开始下载的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        //如果没有生成dataTask,则调用完成回调,并传递error
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }

#if SD_UIKIT
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

最后,我们再来看看SDWebImageManager中调用imageDownloader工具类生成downloadToken的完成回调中做了什么事情:

__weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                if (!strongSubOperation || strongSubOperation.isCancelled) {
                    // 如果strongSubOperation为空,或者被取消了,什么都不做
                } else if (error) {
                  //如果产生错误,则执行完成回调,并回传错误
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL;
                    // 检查是否需要将当前的url放到请求失败的url数组中
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }
                else {
                    //一切正常的话就走到这里
                    if ((options & SDWebImageRetryFailed)) {
                    //如果options设置了SDWebImageRetryFailed选项,就把当前url从failedURLs中移除。因为有可能多次请求一个url,前面请求失败的话,就被添加到这个数组中了。请求成功的时候需要移除。
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // SD自己的manager是默认实现了缩放处理的,如果使用的是用户自己的manager就走下面这步进行缩放处理
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                            //异步执行图片转换操作
                            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);
                                }
                                [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];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //如果用户自定义了图片的缓存处理方法
                            if (self.cacheSerializer) {
                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                });
                            } else {
                                //将图片存入缓存
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //调用最终的完成回调
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    //如果操作完成,将当前操作从保存的正在运行的操作数组中移除
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];

至此,SD的所有主流程我们都梳理了一遍。这一路的参数传递及各种block回调,刚开始看的时候确实会比较懵逼。但是只要静下心来,对照着源码耐心研读,最终一定会融会贯通的。当然,SD还有一些其他的模块,我自己也没有仔细去看,就不班门弄斧了。第三方源码的解读确实是比较花时间,特别是想自己写一篇比较全面的总结得时候就更加需要耐心了。一不小心这篇总结就差不多花了我周末两天时间,已经周日下午三点多,是时候抓住周末的尾巴啦~就酱,溜了溜了。。。


嚣张.jpg

参考资料:
https://knightsj.github.io/2018/02/03/SDWebImage%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
https://www.jianshu.com/p/9e97c11aeea9

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容