SDWebImage5.11源码分析(一)

SDWebImage5.0进行了一次架构上的改进,因为转Swift之后,一直没用到SDWebImage,所以也没怎么关注。最近有空刚好学习一下SDWebImage5.11的源码。

一、流程架构图
流程架构图

  • SDWebImage对UIButton,UIImageView,NSButton,UIView进行了拓展,并对外提供了接口。无论对UIButton,UIImageView还是NSButton调用sd_setImageWithURL的时候,最终都会调用到UIView拓展类的sd_internalSetImageWithURL方法。

  • 前面的拓展都只是对外的接口,主要逻辑处理放在SDWebImageManager里面。他相当于一个调度中心,如果需要缓存(读跟取),他就会调用SDImageCache,如果需要下载,就会调用SDWebImageDownloader。类似我们MVP模式下的Presenter,收到View拓展接口相关的参数后,根据不同业务传递给cache跟downloader处理,最后将处理完的数据通过block回调给接口。

最后还有一些工具,没有在流程图中画出来,这里说明一下:

  • Decoder:做一些编解码操作,针对不同类型的图片进行不同的操作。

  • Transform:从缓存或下载转换图像加载的转换器协议。

  • AnimatedImage:可以替代UIImageView,支持gif

  • Utils:存放一些枚举,Define,还有菊花器

  • Categories:对需要的类进行拓展,大部分是UIImage

  • Private:一些私人方法

二、代码部分

1. UIView+WebCache

直接找到sd_internalSetImageWithURL方法,这是入口进来后第一个处理的方法,处理内容如下:

a. 拿到旧的operation(任务),取消其操作,并从SDOperationsDictionary移除。然后创建新的加载任务,并加入到SDOperationsDictionary中。

b. 处理进度条,重置进度条

c. 处理菊花器

d. 创建SDWebImageManager,并调用loadImageWithURL加载图片

我们先看sd_internalSetImageWithURL方法里面的代码

a、取消之前的任务
/*
     *通过SDWebImageContextSetImageOperationKey拿到SDOperationsDictionary的key:validOperationKey(说白了这里就是二维字典)
     *在通过validOperationKey拿到对应的Operation(任务),对任务进行取消之类的相关操作
     */
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        validOperationKey = NSStringFromClass([self class]);
        // 对context进行深拷贝,转为可变字典
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        // 将当前类对象名称装载进mutableContext
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        // 将mutableContext转回不可变字典context
        context = [mutableContext copy];
    }
    // 将validOperationKey存储起来
    self.sd_latestOperationKey = validOperationKey;
    // 如果这个key存在任务,则取消任务,且从SDOperationsDictionary移除
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    // 将url存储起来
    self.sd_imageURL = url;

这段代码主要是拿到validOperationKey,并传给sd_cancelImage方法,sd_cancelImage的逻辑也很简单,通过validOperationKey,在SDOperationsDictionary里面拿到对应的任务,并取消。下面是sd_cancelImage的代码:

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id operation;
        // 拿到对应的任务operation
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                // 如果存在并遵循SDWebImageOperation代理,则取消任务
                [operation cancel];
            }
            // 最后将任务移除
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
b、占位图显示
// 是否需要延迟加载占位图
    if (!(options & SDWebImageDelayPlaceholder)) {
        // 主线程显示占位图
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
c、进度条,菊花器处理逻辑
if (url) {
        // reset the progress
        // 重置进度条
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        // 有菊花器就转菊花
        [self sd_startImageIndicator];
        id imageIndicator = self.sd_imageIndicator;
#endif
        // 拿到当前manager,没有就创建,有的话就将context的移除,防止循环引用
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }

        // 对进度条进行处理,如果菊花器是进度条类型的,那就让进度条跑起来,回调进度block
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

注意这里并没有直接调用combinedProgressBlock处理进度条,而是在下面加载图片的时候将combinedProgressBlock扔过去处理。

d、通过SDWebImageManager调用加载图片的方法

这里调用了SDWebImageManager的图片加载方法,将一些必要参数传递过去,接下来就是SDWebImageManager的事情了。

[manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
2. SDWebImageManager

直接找到loadImageWithURL方法,这个方法主要是对url的一些判断,contextoptions的预处理,内容如下:

a. 先判断url的可行性

b. 对contextoptions进行预处理,并放到result里面

c. 调用callCacheProcessForOperation 判断是否有缓存,如果有则进入ImageCache 拿到缓存数据,如果没有则进入callDownloadProcessForOperation 方法进一步判断如何下载

先看看这些步骤的源码,看完再看callCacheProcessForOperation做了些什么

a、判断url的可行性
// Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    // 可以把operation当做是一个任务,一个执行着读取图片(缓存跟加载器的组合)操作的任务
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    BOOL isFailedUrl = NO;
    // 检测当前url是否在failedURLs列表中
    if (url) {
        //os_unfair_lock的宏定义
        // 加锁,防止多个线程对failedURLs操作,引起的数据问题
        SD_LOCK(_failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }

    // 如果url为nil,且未设置SDWebImageRetryFailed,url在failedURLs列表中,执行失败回调
    // SDWebImageRetryFailed为失败链接重试,默认是不会重试
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

这里注释应该很清楚了,就是判断url的可行性,跟url是否在失败列表里面,如果在的,且options没有SDWebImageRetryFailed的话,就直接失败回调。值得注意的是SD的锁在iOS10以上用的是os_unfair_lock,iOS10以下用的是OSSpinLockLock(这个锁存在任务优先级问题,已经被淘汰了)

b、对contextoptions进行预处理,并放到result里面
// 将当前operation加入到runningOperations(正在运行的operation)
    // 加锁,防止多个线程对runningOperations进行操作
    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    // 对context进行预处理,然后将处理的context跟options包装到result里面。
    /* 里面对context的处理包括,SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer。分别查看外面是否自定义这3个key的context,如果有就使用,没有就使用SD默认的。除了SDWebImageContextCacheKeyFilter(缓存url的key)默认是本身的url,其他2个都是nil
     */
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

这里先将任务加入到正在执行的列表里面,然后再对context进行预处理,源代码是没有对options进行说明处理的,然后将contextoptions放入result里面。context的处理源代码就不贴出来了,大概就是对SDWebImageContextImageTransformerSDWebImageContextCacheKeyFilterSDWebImageContextCacheSerializer这3个进行一个判断,看是否有自定义的传过来,没有就用默认的。

c、callCacheProcessForOperation的调用

这里主要是判断要到哪里去取数据,ImageCache,还是去下载,接下来就进入这个方法看一下。

这里主要是判断任务是否该走缓存查询,或者直接下载。如果是缓存查询,就进入SDImageCache里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation进入下一步判断。

①. 拿到imageCache,拿到缓存类型queryCacheType

②. 通过 options判断,走缓存还是下载。如果走缓存,则调用SDImageCache里面的queryImageForKey(开始进入SDImageCache的逻辑);如果走下载,则调用callDownloadProcessForOperation开始下载前的一些处理。

①、拿到imageCache,拿到缓存类型queryCacheType
// Grab the image cache to use
    // 查看是否有传进来的自定义缓存对象,没有就用默认的imageCache
    id imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // Get the query cache type
    // 查看缓存类型,默认是all,如果有传进来的就用传进来的
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
②、通过options,判断缓存查找,还是下载
// Check whether we should query cache
    // SD_OPTIONS_CONTAINS为与运算,当options为SDWebImageFromLoaderOnly时为true(或者全是1也可以)
    // 注意这里是取反,也就是设置了SDWebImageFromLoaderOnly后是不走缓存,直接下载
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        // 拿到缓存的key
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        // 缓存查询,并返回缓存任务
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            // 如果没有执行的任务,或者任务被取消了
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                // 抛出错误
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                // 安全的移除任务
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
                // 没拿到缓存图片,且图片有经过Transformer转化,那就去查询原始图片缓存
                //有机会去查询原始缓存
                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                return;
            }
            
            // Continue download process
            // 走下载流程
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {// 走下载流程
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }

这里解释一下key是怎么拿(SDWebImage的缓存key是怎么样的),逻辑在这个方法里面cacheKeyForURL,代码就不贴出来了,说一下大概逻辑。

a、SDWebImagecontext里面有个SDWebImageContextCacheKeyFilter,里面存储的是用来存放自定义key逻辑的协议,通过重写cacheKeyForURL自定义key,如果没有传SDWebImageContextCacheKeyFilter进来则使用url的string值。
b、然后通过context里面的SDWebImageContextImageThumbnailPixelSizeSDWebImageContextImagePreserveAspectRatioSDWebImageContextImageTransformer这3个里面是否有值,如果有值就加上上面的key进行拼接,没值就直接用上面的key

查到缓存后就是回调了,回调看代码注释,问题应该不大,要注意的是它也走了callDownloadProcessForOperation这个方法,因为optionsSDWebImageRefreshCached的情况下,也是要走下载的,所以索性将找到的缓存,放到callDownloadProcessForOperation处理,而不是直接回调。
接下来看一下SDImageCache模块,看看SDWebImage是如何查询缓存的。

你可能感兴趣的:(SDWebImage5.11源码分析(一))