美文网首页
SDWebImage(v4.4.2)源码学习及知识点分析

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

作者: YouKnowZrx | 来源:发表于2018-10-28 15:20 被阅读0次

    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

    相关文章

      网友评论

          本文标题:SDWebImage(v4.4.2)源码学习及知识点分析

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