网络知识 娱乐 【iOS开发】—— SDWebImage源码学习(未完)

【iOS开发】—— SDWebImage源码学习(未完)

文章目录

    • 什么是SDWebImage?
    • sd_setImageWithURL调用关系
      • 步骤一
      • 步骤三
      • 步骤四
      • 步骤五
      • 步骤六
    • 下载步骤
        • UIImageView+ WebCache
        • UIView+ WebCache
          • 第一块:
          • 第二块:
          • 第三块:
          • 第四块:
          • 第五块
          • 第六块
          • 第七块
          • 第八块
          • 总结
    • 相关类名与功能描述
    • 缓冲
      • 内存缓冲
      • 磁盘缓冲
      • 清理缓冲的策略
    • 相关问题

什么是SDWebImage?

SDWebImage是iOS开发中被广泛使用的第三方开源库,它提供了图片从加载、解析、缓存、清理等一系列功能。

平时我们引进这个第三方库,最主要的就是使用sd_setImageWithURL方法。下面讲讲这个方法的调用

sd_setImageWithURL调用关系

在这里插入图片描述

通过上图,可以知道SDWebImage加载的过程是首先从缓存中加载数据,缓存加载是优先从内存缓存中加载,然后才是磁盘加载。如果没有缓存,才从网络上加载。网络成功加载图片以后,存入本地缓存。

步骤一

根据自己需要选择UIImageView+WebCache类中以下任一方法:

这个类主要是实现以下方法:

- (void)sd_setImageWithURL:(nullable NSURL *)url;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder;
          
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;
                   
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context;
                   
- (void)sd_setImageWithURL:(nullable NSURL *)url
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;                

其实以上所有的方法都是在调用下面这个方法:

- (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;

而这个方法则是调用了UIView+WebCache类中的方法。

步骤三

上面UIImageView+WebCache调用的就是UIView+WebCache类中的

- (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;

在这个方法里,创建了一个SDWebImageManager对象,然后去调用其类的loadImageWithURL:()

所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有UIView及其子类的分类都是用这个类的来实现图片的加载,这个类也是主要实现了这个方法。

步骤四

上面的方法会调用SDWebImageManager类的:

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock;

SDWebImageManager 对象的上述方法里,首先会查询在缓存中有没有这个图片,然后根据各种 option 判断决定是否要从网络端下载。而查询缓存中有没有是通过调用 SDImageCache 对象的实例方法来实现的。

步骤五

上面的方法会调用SDImageCache类的下面方法从图像缓存中查询给定密钥的缓存图像,如果映像缓存在内存中,则同步调用完成;否则异步调用完成。

- (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
                                             options:(SDWebImageOptions)options
                                             context:(nullable SDWebImageContext *)context
                                           cacheType:(SDImageCacheType)cacheType
                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;

SDImageCache 这个类是专门负责缓存相关的问题的,包括查询缓存和将图片进行缓存。SDImageCache 使用了一个 NSCache 对象来进行内存缓存,磁盘缓存则是把图片数据存放在应用沙盒的Caches这个文件夹下。

这里的查询缓存的步骤为:首先查询内存缓存,内存缓存查询完了以后再判断是否需要查询磁盘缓存。如果查询内存缓存已经有了结果并且没有设置一定要查询磁盘缓存,那么就不查询磁盘缓存,否则就要查询磁盘缓存。内存缓存没有查询到图片,并且磁盘缓存查询到了图片,那么就要把这个内容缓存到内存缓存中,返回缓存查询的结果;如果磁盘缓冲也没有查询到图片,就调用下一个类去下载图片。

步骤六

下载未缓冲过的图片时,使用类SDWebImageDownloader下面这个方法:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

下载步骤

整个步骤就是下面这样:
在这里插入图片描述

UIImageView+ WebCache

这个分类在上面介绍调用关系时已经说过了,里面的方法都是在为程序员的不同需求提供对外的接口方法,最后都是调用

- (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;

这个方法,没什么说的。

UIView+ WebCache

这个类分析一下- (void)sd_internalSetImageWithURL:()方法:

- (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 {
    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    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]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    self.sd_latestOperationKey = validOperationKey;
    
     //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    // 获取图像管理者对象
    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];
    }
    
    BOOL shouldUseWeakCache = NO;
    if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
        shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
    }
    if (!(options & SDWebImageDelayPlaceholder)) {
        if (shouldUseWeakCache) {
            NSString *key = [manager cacheKeyForURL:url context:context];
            // call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
            // this unfortunately will cause twice memory cache query, but it's fast enough
            // in the future the weak cache feature may be re-design or removed
            [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
        }
        dispatch_main_async_safe(^{
        //设置下载图片完成之前的占位图
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    // 判断url是否有效
    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<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        
        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);
            }
        };
        @weakify(self);
        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 callCompletedBlockClosure = ^{
                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(callCompletedBlockClosure);
                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;
            BOOL shouldUseTransition = NO;
            if (options & SDWebImageForceTransition) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don't use sync query)
                if (cacheType == SDImageCacheTypeMemory) {
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) {
                    if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    shouldUseTransition = NO;
                }
            }
            if (finished && shouldUseTransition) {
                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
                callCompletedBlockClosure();
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
        [self sd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}

一行一行分析一下:
首先是方法名:
其总共有五个参数,URL就是我们需要下载的在线图片链接,placeholder(占位符)Image其是UIImage类型,而SDWebImageOptions我们查看其源码并进行相关信息的查询
其是一种暴露在外的可供使用者使用的选择方法

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     *默认情况下,当URL下载失败时,该URL会被列入黑名单,因此库不会继续尝试。
	 *此标志禁用此黑名单。
     */
    SDWebImageRetryFailed = 1 << 0,
    
    /**
     *默认情况下,图像下载是在UI交互期间启动的,此标志禁用此功能,
	 *例如,导致在UIScrollView上延迟下载。
     */
    SDWebImageLowPriority = 1 << 1,
    
    /**
    *此标志启用渐进式下载,图像在下载过程中会像浏览器一样渐进式显示。
	*默认情况下,图像仅在完全下载后显示。
     */
    SDWebImageProgressiveLoad = 1 << 2,
    
    /**
    *即使映像已缓存,也要遵守HTTP响应缓存控制,并在需要时从远程位置刷新映像。
	*磁盘缓存将由NSURLCache而不是SDWebImage处理,这会导致性能略有下降。
	*此选项有助于处理同一请求URL后面更改的图像,例如Facebook graph api配置文件图片。
	*如果刷新了缓存的图像,则使用缓存的图像调用一次完成块,然后使用最终图像调用一次完成块。
	*
	*只有当不能使用嵌入式缓存破坏参数使URL保持静态时,才使用此标志。
    */
    SDWebImageRefreshCached = 1 << 3,
    
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 4,
    
    /**
    *通过设置
	*NSMutableURLRequest。HTTPShouldHandleCookies=是;
     */
    SDWebImageHandleCookies = 1 << 5,
    
    /**
    *启用以允许不受信任的SSL证书。
	*用于测试目的。在生产中小心使用。
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 6,
    
    /**
    *默认情况下,图像按其排队顺序加载。此标志将它们移动到
	*排在队伍前面。
     */
    SDWebImageHighPriority = 1 << 7,
    
    /**
     *默认情况下,在加载图像时加载占位符图像。此标志将延迟加载
	*直到图像加载完毕。
     */
    SDWebImageDelayPlaceholder = 1 << 8,
    
    /**
     *我们通常不会对动画图像应用变换,因为大多数变形金刚无法管理动画图像。
	 *无论如何都要使用此标志来变换它们。
     */
    SDWebImageTransformAnimatedImage = 1 << 9,
    
    /**
     *默认情况下,图像在下载后添加到imageView。但在某些情况下,我们希望
	*在设置图像之前先用手(例如,应用过滤器或添加交叉淡入淡出动画)
	*如果要在成功时手动设置完成中的图像,请使用此标志
     */
    SDWebImageAvoidAutoSetImage = 1 << 10,
    
    /**
     *默认情况下,图像将根据其原始大小进行解码。
	*此标志将图像缩小到与设备受限内存兼容的大小。
	*要控制内存字节的限制,请选中“SDImageCoderHelper”。defaultScaleDownLimitBytes`(在iOS上默认为60MB)
	*这实际上将转换为使用上下文选项“”。imageThumbnailPixelSize`来自v5。5.0(在iOS上默认为(39663966)。以前没有。
	*以及v5和动画效果中的标志。5.0. 以前没有。
	*@note如果你需要细节控件,最好使用上下文选项“imageThumbnailPixelSize”和“ImagePreserveApectRatio”。
     */
    SDWebImageScaleDownLargeImages = 1 << 11,
    
    /**
    *默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。这个掩码可以强制同时查询图像数据。但是,除非指定'SDWebImageQueryMemoryDataSync',否则此查询是异步的`
     */
    SDWebImageQueryMemoryData = 1 << 12,
    
    /**
     *默认情况下,当您只指定'SDWebImageQueryMemoryData'时,我们会异步查询内存图像数据。并结合该掩码同步查询内存图像数据。
	*@note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元重用期间闪烁。
     */
    SDWebImageQueryMemoryDataSync = 1 << 13,
    
    /**
     *默认情况下,当内存缓存丢失时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
	*@note这3个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参