美文网首页iOS
[iOS] SDWebImage 源码分析(1)

[iOS] SDWebImage 源码分析(1)

作者: 木小易Ying | 来源:发表于2020-02-29 12:58 被阅读0次

    git: https://github.com/SDWebImage/SDWebImage

    这个库是一直很想看但是没时间看的一个库啦,我之前都是用sd_setImageWithURL比较多,所以从这个点入手看了~

    SD里面很多组件都加了category,方便我们直接在UIbutton之类的用setImage,常用的还是imageView:

    @implementation UIImageView (WebCache)
    
    - (void)sd_setImageWithURL:(nullable NSURL *)url {
        [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
    }
    

    最后调到得的是:

    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options
                       context:(nullable SDWebImageContext *)context
                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
                     completed:(nullable SDExternalCompletionBlock)completedBlock {
        [self sd_internalSetImageWithURL:url
                        placeholderImage:placeholder
                                 options:options
                                 context:context
                           setImageBlock:nil
                                progress:progressBlock
                               completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                                   if (completedBlock) {
                                       completedBlock(image, error, cacheType, imageURL);
                                   }
                               }];
    }
    

    这个超级庞大的方法最后调回了UIView的方法:

    // @interface UIView (WebCache)
    /**
     * Set the imageView `image` with an `url` and optionally a placeholder image.
     *
     * The download is asynchronous and cached.
     *
     * @param url            The url for the image.
     * @param placeholder    The image to be set initially, until the image request finishes.
     * @param options        The options to use when downloading the image. @see SDWebImageOptions for the possible values.
     * @param context        A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
     * @param setImageBlock  Block used for custom set image code. If not provide, use the built-in set image code (supports `UIImageView/NSImageView` and `UIButton/NSButton` currently)
     * @param progressBlock  A block called while image is downloading
     *                       @note the progress block is executed on a background queue
     * @param completedBlock A block called when operation has been completed.
     *   This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter.
     *   In case of error the image parameter is nil and the third parameter may contain an NSError.
     *
     *   The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
     *   or from the memory cache or from the network.
     *
     *   The fith parameter normally is always YES. However, if you provide SDWebImageAvoidAutoSetImage with SDWebImageProgressiveLoad options to enable progressive downloading and set the image yourself. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
     *   block is called a last time with the full image and the last parameter set to YES.
     *
     *   The last parameter is the original image URL
     */
    - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                      placeholderImage:(nullable UIImage *)placeholder
                               options:(SDWebImageOptions)options
                               context:(nullable SDWebImageContext *)context
                         setImageBlock:(nullable SDSetImageBlock)setImageBlock
                              progress:(nullable SDImageLoaderProgressBlock)progressBlock
                             completed:(nullable SDInternalCompletionBlock)completedBlock;
    

    这个方法实在是太大了,所以后面会分成小块儿来看~


    1. 找之前的operation先取消掉

    context = [context copy]; // copy to avoid mutable object
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
    }
    self.sd_latestOperationKey = validOperationKey;
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    

    这里的context是SDWebImageContext类,这个类是干啥的呢?

    typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
    typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
    typedef NSMutableDictionary<SDWebImageContextOption, id> SDWebImageMutableContext;
    

    SDWebImageContext就是一个字典,装着很多图片的加载线程key、各种相关类的identifier这种感觉(其实就是有东西要和image绑定就可以放进context里面),可以放哪些嘞:

    #pragma mark - Context option
    
    SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey";
    SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
    SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer";
    SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor";
    SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio";
    SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
    SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
    SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType";
    SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass";
    SDWebImageContextOption const SDWebImageContextDownloadRequestModifier = @"downloadRequestModifier";
    SDWebImageContextOption const SDWebImageContextDownloadResponseModifier = @"downloadResponseModifier";
    SDWebImageContextOption const SDWebImageContextDownloadDecryptor = @"downloadDecryptor";
    SDWebImageContextOption const SDWebImageContextCacheKeyFilter = @"cacheKeyFilter";
    SDWebImageContextOption const SDWebImageContextCacheSerializer = @"cacheSerializer";
    

    所以有的时候如果需要存很多东西,又不想搞成属性,毕竟太多属性很乱,可以试着用context,key可以用option(NS_EXTENSIBLE_STRING_ENUM的字符串枚举类型)定义好。

    关于NS_EXTENSIBLE_STRING_ENUM可以参考https://blog.csdn.net/weixin_34358092/article/details/91424409,是苹果用于桥接swift的字符串枚举做的,注意需要先typedef为一个类型使用哦。

    既然context是NSDictionary,自然需要先copy一下防止传入的是NSMutableDictionary,然后从里面拿SDWebImageContextSetImageOperationKey对应的value,拿到以后干了什么嘞?

    - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
        if (key) {
            // Cancel in progress downloader from queue
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            id<SDWebImageOperation> operation;
            
            @synchronized (self) {
                operation = [operationDictionary objectForKey:key];
            }
            if (operation) {
                if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                    [operation cancel];
                }
                @synchronized (self) {
                    [operationDictionary removeObjectForKey:key];
                }
            }
        }
    }
    
    =============
    
    @implementation UIView (WebCacheOperation)
    
    typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
    
    - (SDOperationsDictionary *)sd_operationDictionary {
        @synchronized(self) {
            SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
            if (operations) {
                return operations;
            }
            operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
            objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            return operations;
        }
    }
    

    所以其实就是每个UIview有一个关联对象NSMapTable存了所有id<SDWebImageOperation>,key就是我们从context里面拿到的SDWebImageContextSetImageOperationKey所对应的value,当加载图片一开始,先要cancel这个operation,并且从关联对象的NSMapTable里面移除。

    注意这里NSMapTable也是非线程安全的,所以所有对operationDictionary的操作都包了一层@synchronized (self)

    另外从上面可以看出,sd_operationDictionary存的应该是正在进行中的operations,所以cancel以后要移除。


    2. 加载占位图

    默认情况下,在加载图像时,占位图像已经会被加载。options含有SDWebImageDelayPlaceholder会延迟加载占位图像,直到图像已经完成加载,所以如果没有置位,则先加载占位图:

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    

    这里dispatch_main_async_safe是保证代码会在主队列执行,而非主线程哦注意。

    #ifndef dispatch_main_async_safe
    #define dispatch_main_async_safe(block)\
        if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
            block();\
        } else {\
            dispatch_async(dispatch_get_main_queue(), block);\
        }
    #endif
    

    这个可以参考一下:http://blog.benjamin-encz.de/post/main-queue-vs-main-thread/,如果在主线程执行非主队列调度的API,而这个API需要检查是否由主队列上调度,那么将会出现问题。(例如MapKit / VektorKit当不是主队列会返回nil可能会导致crash)这篇文章通过dispatch_queue_set_specific 给主队列设置key value对来标志,异曲同工用label

    然后具体看一下加载占位图所调用的方法:

    - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
    #if SD_UIKIT || SD_MAC
        [self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL];
    #else
        // watchOS does not support view transition. Simplify the logic
        if (setImageBlock) {
            setImageBlock(image, imageData, cacheType, imageURL);
        } else if ([self isKindOfClass:[UIImageView class]]) {
            UIImageView *imageView = (UIImageView *)self;
            [imageView setImage:image];
        }
    #endif
    }
    

    Apple Watch不支持transition相关的API,所以这里直接setImage即可,其他的情况会回调下面的,如果我们设置了setImageBlock,那么view的显示图片的逻辑应该写到这个Block中,如果setImageBlock为nil,就判断是不是UIImageView和UIButton:

    - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
        UIView *view = self;
        SDSetImageBlock finalSetImageBlock;
        if (setImageBlock) {
            finalSetImageBlock = setImageBlock;
        } else if ([view isKindOfClass:[UIImageView class]]) {
            UIImageView *imageView = (UIImageView *)view;
            finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
                imageView.image = setImage;
            };
        }
    #if SD_UIKIT
        else if ([view isKindOfClass:[UIButton class]]) {
            UIButton *button = (UIButton *)view;
            finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
                [button setImage:setImage forState:UIControlStateNormal];
            };
        }
    #endif
    #if SD_MAC
        // NSButton的处理
    #endif
        
        if (transition) {
    #if SD_UIKIT
            [UIView transitionWithView:view duration:0 options:0 animations:^{
                // 0 duration to let UIKit render placeholder and prepares block
                if (transition.prepares) {
                    transition.prepares(view, image, imageData, cacheType, imageURL);
                }
            } completion:^(BOOL finished) {
                [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                    if (finalSetImageBlock && !transition.avoidAutoSetImage) {
                        finalSetImageBlock(image, imageData, cacheType, imageURL);
                    }
                    if (transition.animations) {
                        transition.animations(view, image);
                    }
                } completion:transition.completion];
            }];
    #elif SD_MAC
            // 用NSAnimationContext做transition
            ……
    #endif
        } else {
            if (finalSetImageBlock) {
                finalSetImageBlock(image, imageData, cacheType, imageURL);
            }
        }
    }
    

    3. 进度设置

    当有url的时候,每个对象有一个NSProgress的关联对象,保存着图片加载的进度,如果还有关联的indicator,还会更新indicator来展示进度给用户:

    // reset the progress
    // 拿关联的NSProgress重置为初始状态
    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_imageIndicator也就是关联的sd_imageIndicator对象不为nil则start animation
    [self sd_startImageIndicator];
    id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
    #endif
    
    // 如果context里面没有SDWebImageContextCustomManager指定特定的manager,则用shared manager
    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
    if (!manager) {
        manager = [SDWebImageManager sharedManager];
    }
    
    // 新建一个combinedProgressBlock更新imageProgress
    // 如果有imageIndicator会调用updateIndicatorProgress展示progress
    // 如果progressBlock参数不为nil,还要把进度传给外面
    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);
        }
    };
    

    4. 创建operation

    @weakify(self);
    // 上面的combinedProgressBlock会在这里传给loadImageWithURL作为回调
    id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        @strongify(self);
        if (!self) { return; }
        // if the progress not been updated, mark it to complete state
        if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
            imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
            imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
        }
        
    #if SD_UIKIT || SD_MAC
        // check and stop image indicator
        if (finished) {
            [self sd_stopImageIndicator];
        }
    #endif
        
        BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
        BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                  (!image && !(options & SDWebImageDelayPlaceholder)));
        SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
            if (!self) { return; }
            if (!shouldNotSetImage) {
                [self sd_setNeedsLayout];
            }
            if (completedBlock && shouldCallCompletedBlock) {
                completedBlock(image, data, error, cacheType, finished, url);
            }
        };
        
        // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
        // OR
        // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
        if (shouldNotSetImage) {
            dispatch_main_async_safe(callCompletedBlockClojure);
            return;
        }
        
        UIImage *targetImage = nil;
        NSData *targetData = nil;
        if (image) {
            // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
            targetImage = image;
            targetData = data;
        } else if (options & SDWebImageDelayPlaceholder) {
            // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
            targetImage = placeholder;
            targetData = nil;
        }
        
    #if SD_UIKIT || SD_MAC
        // check whether we should use the image transition
        SDWebImageTransition *transition = nil;
        if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
            transition = self.sd_imageTransition;
        }
    #endif
        dispatch_main_async_safe(^{
    #if SD_UIKIT || SD_MAC
            [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
    #else
            [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
    #endif
            callCompletedBlockClojure();
        });
    }];
    [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    

    这段作者的注释已经超棒了就简单过一下,首先会finish掉imageProgress和imageIndicator,然后就分了四种情况:

    • 获取到了image并且有设置SDWebImageAvoidAutoSetImage,直接调用callCompletedBlockClojure
    • 获取到了image并且没设置SDWebImageAvoidAutoSetImage,调用sd_setImage方法设置图片,然后调用callCompletedBlockClojure
    • 没image并且没设置SDWebImageDelayPlaceholder,直接调用callCompletedBlockClojure
    • 没image并且有设置SDWebImageDelayPlaceholder,调用sd_setImage方法设置占位图,然后调用callCompletedBlockClojure

    最后把这个operation放到之前的SDOperationsDictionary里面。注意这里设置图片的方法和上面设置占位图的是一样的。


    5. SDWebImageManager的loadImageWithURL

    - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                              context:(nullable SDWebImageContext *)context
                                             progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                            completed:(nonnull SDInternalCompletionBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        // completedBlock不能为空
        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.
        // 如果url是字符串,转NSString为NSURL
        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;
        }
    
        SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        operation.manager = self;
    
        // 这里是去看这个url是不是之前fail过
        // SD_LOCK就是#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
        // failedURLsLock是dispatch_semaphore_t信号量,和YYmodel里面的很像都用信号量来保证单一线程访问
        BOOL isFailedUrl = NO;
        if (url) {
            SD_LOCK(self.failedURLsLock);
            isFailedUrl = [self.failedURLs containsObject:url];
            SD_UNLOCK(self.failedURLsLock);
        }
    
        // 如果url长度为0,或者之前失败过并且option没有SDWebImageRetryFailed则error return
        if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
            return operation;
        }
    
        SD_LOCK(self.runningOperationsLock);
        [self.runningOperations addObject:operation];
        SD_UNLOCK(self.runningOperationsLock);
        
        // Preprocess the options and context arg to decide the final the result for manager
        SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
        
        // Start the entry to load image from cache
        [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
    
        return operation;
    }
    

    这段用了两次dispatch_semaphore_t作为锁来确保单线程访问dict,这个YYModel也用过,可以借鉴一下哈~

    总体而言这段就是创建了SDWebImageCombinedOperation,并且尝试从cache里面拿image:

    - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(nullable SDWebImageContext *)context
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Check whether we should query cache
        BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
        if (shouldQueryCache) {
            NSString *key = [self cacheKeyForURL:url context:context];
            @weakify(operation);
            operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context 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:nil] url:url];
                    [self safelyRemoveOperationFromRunning:operation];
                    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];
        }
    }
    

    如果options的SDWebImageFromLoaderOnly置位了,就不query缓存直接call下载,如果query就是先通过url拿到cache的key,然后开始通过key找缓存,最后call下载并将缓存传过去。


    6. callDownloadProcessForOperation

    // Download process
    - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                        url:(nonnull NSURL *)url
                                    options:(SDWebImageOptions)options
                                    context:(SDWebImageContext *)context
                                cachedImage:(nullable UIImage *)cachedImage
                                 cachedData:(nullable NSData *)cachedData
                                  cacheType:(SDImageCacheType)cacheType
                                   progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                  completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Check whether we should download image from network
        // 如果options含有SDWebImageFromCacheOnly则不下载
        BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    
        // 如果有cache并且SDWebImageRefreshCached不置位1则不下载
        shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    
        // 如果有delegate实现了shouldDownloadImageForURL并返回false不下载
        shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    
        // 如果canRequestImageForURL返回false不下载
        shouldDownload &= [self.imageLoader canRequestImageForURL:url];
    
    
        if (shouldDownload) {
            // 如果有cache并且SDWebImageRefreshCached为1则吧cache放入context
            if (cachedImage && options & SDWebImageRefreshCached) {
                // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
                SDWebImageMutableContext *mutableContext;
                if (context) {
                    mutableContext = [context mutableCopy];
                } else {
                    mutableContext = [NSMutableDictionary dictionary];
                }
                mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
                context = [mutableContext copy];
            }
            
            @weakify(operation);
            operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                @strongify(operation);
                if (!operation || operation.isCancelled) {
                    // Image combined operation cancelled by user
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
                } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                    // Image refresh hit the NSURLCache cache, do not call the completion block
                } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                    // Download operation cancelled by user before sending the request, don't block failed URL
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                } else if (error) {
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
                    
                    if (shouldBlockFailedURL) {
                        SD_LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        SD_UNLOCK(self.failedURLsLock);
                    }
                } else {
                    if ((options & SDWebImageRetryFailed)) {
                        SD_LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        SD_UNLOCK(self.failedURLsLock);
                    }
                    // Continue store cache process
                    // 如果成功了就存到缓存
                    [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
                }
                
                if (finished) {
                    [self safelyRemoveOperationFromRunning:operation];
                }
            }];
        } else if (cachedImage) {
            // 无需下载且有cache
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // Image not in cache and download disallowed by delegate
            [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }
    

    7. loaderOperation

    imageLoader的requestImageWithURL返回一个id<SDWebImageOperation>,里面其实就是转换了一下SDWebImageOptions为SDWebImageDownloaderOptions,例如酱紫:

    if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
    

    然后最后其实还是调用的:

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                       context:(nullable SDWebImageContext *)context
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        // 如果url是空则返回
        ……
        
        SD_LOCK(self.operationsLock);
        id downloadOperationCancelToken;
        NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
        // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
        if (!operation || operation.isFinished || operation.isCancelled) {
            operation = [self createDownloaderOperationWithUrl:url options:options context:context];
            if (!operation) {
                // operation为nil返回
            }
            @weakify(self);
            operation.completionBlock = ^{
                @strongify(self);
                if (!self) {
                    return;
                }
                SD_LOCK(self.operationsLock);
                [self.URLOperations removeObjectForKey:url];
                SD_UNLOCK(self.operationsLock);
            };
            self.URLOperations[url] = operation;
            // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
            // Add operation to operation queue only after all configuration done according to Apple's doc.
            // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
            [self.downloadQueue addOperation:operation];
        } else {
            // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
            // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
            @synchronized (operation) {
                downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
            }
            if (!operation.isExecuting) {
                // 如果没有在执行设置优先级
                if (options & SDWebImageDownloaderHighPriority) {
                    operation.queuePriority = NSOperationQueuePriorityHigh;
                }
                ……
            }
        }
        SD_UNLOCK(self.operationsLock);
        
        SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
        token.url = url;
        token.request = operation.request;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
        
        return token;
    }
    

    这里用NSOperation<SDWebImageDownloaderOperation>创建了一个SDWebImageDownloadToken返回的,operation的创建是酱紫的:

    - (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                      options:(SDWebImageDownloaderOptions)options
                                                                                      context:(nullable SDWebImageContext *)context {
        // 设置超时时间
        NSTimeInterval timeoutInterval = self.config.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        
        // 设置cache policy
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
        mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
        mutableRequest.HTTPShouldUsePipelining = YES;
        SD_LOCK(self.HTTPHeadersLock);
        mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
        SD_UNLOCK(self.HTTPHeadersLock);
        
        // 设置Context
        SDWebImageMutableContext *mutableContext;
        if (context) {
            mutableContext = [context mutableCopy];
        } else {
            mutableContext = [NSMutableDictionary dictionary];
        }
        
        // 从context里面拿Request Modifier
        id<SDWebImageDownloaderRequestModifier> requestModifier;
        if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
            requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
        } else {
            requestModifier = self.requestModifier;
        }
        
        NSURLRequest *request;
        if (requestModifier) {
            NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
            // If modified request is nil, early return
            if (!modifiedRequest) {
                return nil;
            } else {
                request = [modifiedRequest copy];
            }
        } else {
            request = [mutableRequest copy];
        }
        
        // 从context里面拿Response Modifier
        // 从context里面拿Decryptor
        ……
        
        context = [mutableContext copy];
        
        // Operation Class
        Class operationClass = self.config.operationClass;
        if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
            // Custom operation class
        } else {
            operationClass = [SDWebImageDownloaderOperation class];
        }
        NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
        
        // 设置证书信任
        if ([operation respondsToSelector:@selector(setCredential:)]) {
            if (self.config.urlCredential) {
                operation.credential = self.config.urlCredential;
            } else if (self.config.username && self.config.password) {
                operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
            }
        }
            
        // 设置setMinimumProgressInterval以及queuePriority
        if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
            operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
        }
    
        // 如果是SDWebImageDownloaderLIFOExecutionOrder,也就是最后一个压栈第一个出,就让所有队列里面的operation依赖最后一个
        if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
            // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
            // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
            for (NSOperation *pendingOperation in self.downloadQueue.operations) {
                [pendingOperation addDependency:operation];
            }
        }
        
        return operation;
    }
    

    最后这点说如果LIFO不能只让倒数第二个依赖于倒数第一个,必须队列里面之前所有的都依赖于最后一个哈~

    所以这段大概率如果不custom创建了一个SDWebImageDownloaderOperation的operation,也就是用这个operation来进行下载的~


    8. 流程梳理

    上面按照调用顺序看了一遍,但是只是大概看了,之后会再仔细看一下结构啊SDWebImageDownloaderOperation之类的。但这里的还是梳理一下大概的流程叭:

    • (1)UIImageView的sd_setImageWithURL转调了UIView的sd_internalSetImageWithURL
    • (2)UIView的sd_internalSetImageWithURL里,先取消了之前的SDWebImageOperation (key是context[SDWebImageContextSetImageOperationKey]或者当前view的class name),注意这里的operation dict是关联给每个view的,所以当同一个image再次setImage的时候会cancel之前同一个key的operation
    • (3)当option不含有SDWebImageDelayPlaceholder的时候UIView通过调用sd_setImage加载占位图先,这里的占位图传入的就是UIImage哈
    • (4)然后创建一个combinedProgressBlock将进度更新回调给调用者以及imageIndicator更新都封成一个block
    • (5)通过manager的loadImageWithURL生成一个operation,将之前创建的combinedProgressBlock、option之类的都传入,并且设置complete的block里将下载好的图片设置上。最后将新建的operation存到object的operation dict里面
    • (6)manager的loadImageWithURL里面创建了一个SDWebImageCombinedOperation加入到runningOperations里,然后去拿cache如果option没有不允许拿的话,再设置operation.loaderOperation
    • (7)operation.loaderOperation是一个下载的operation,SDWebImageCombinedOperation还有一个cacheOperation,这个combine和progress的combine类似,就是把下载 & 存入缓存封成了一个operation

    小姐姐下次还会看SD的,最近可能各种瞎看叭,诶老了老了智商拙计啊

    Refenerce:
    https://www.jianshu.com/p/cfde8db5c051

    相关文章

      网友评论

        本文标题:[iOS] SDWebImage 源码分析(1)

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