【iOS】—— SDWebImage源码学习(2)(源码解读)

【iOS】------ SDWebImage源码学习(2)(源码解读)

1.UIKit层

最外层的是UIImageView+WebCache,在这个层中为我们提供了很多借口。

objectivec 复制代码
- (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 {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed: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 {
    [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);
                               }
                           }];
}
- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock {
    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
    UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
    
    [self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];    
}

这些最终都是调用下面这个全能方法。全能方法除了必需的的图片地址,还提供了占位图、可选项、加载进度和完成回调。

objectivec 复制代码
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

这个全能方法并没有什么实际的实现,还是对另一个分类UIView+WebCache方法的封装,看看全能方法的实现:

objectivec 复制代码
- (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);
                               }
                           }];
}

上面的这个方法也调用了全能方法,下面是其核心实现:

objectivec 复制代码
- (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;
    // 如果没有选择延迟加载占位图
    if (!(options & SDWebImageDelayPlaceholder)) {
    	// 在主线程主队列中设置占位图
        dispatch_main_async_safe(^{
        	// 作为图片下载完成之前的替代图片
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL: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
		// 生成图片管理者对象,如果context中有就用context中的
        SDWebImageManager *manager;
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
		// 否则就直接生成SDWebImageManager单例对象
        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];
        }
        
        // 生成一个代码块用来在下载图片的方法中监听进度并进行回调
        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
            // 是否应该回调完成block: 如果已经完成或者设置了在设置图片前处理
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            // 是否应该不设置图片: 如果有图片但设置了在设置图片前处理,或者没有图片并且没有设置延迟加载占位图
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            // 生成完成回调代码块
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
            	// 如果没有生成强引用的self就终止执行 
                if (!self) { return; }
                // 如果需要设置图片就直接刷新视图
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                // 如果传入了回调block并且应该进行回调,就直接回调
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, 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;
            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) {
                	// SDWebImageQueryMemoryDataSync:
                    // 默认情况下,当您仅指定"SDWebImageQueryMemoryData"时,我们会异步查询内存映像数据。
                    // 将此掩码也组合在一起,以同步查询内存图像数据
                    // 不建议同步查询数据,除非您要确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。
                    // SDWebImageQueryDiskDataSync:
                    // 默认情况下,当内存缓存未命中时,我们会异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
                    // 这两个选项打开则NO。
                    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
                callCompletedBlockClojure();
            });
        }];
        // 根据密钥保存下载图片的操作
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
		// 如果没传入图片链接,就在主线程主队列移除加载小菊花
        [self sd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
        	// 如果传入了完成回调block就回调错误信息
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}

接下来我们讨论一下这段代码的核心步骤。

取消当前正在进行的异步下载

取消当前正在进行的异步下载,确保每个UIImageView对象中永远只存在一个operation,当前只允许一个图片网络请求,该operation负责从缓存中获取image或者是重新下载image。

objectivec 复制代码
// 生成一个有效的操作密钥
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    // 如果传入了参数就用传入的,否则就用当前类的类名
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
// 取消先前下载的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

... // 下载图片操作

// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上述方法定义在UIView+WebCacheOperation类中:

objectivec 复制代码
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        // 从队列中取消正在进行的下载程序

		// 获取添加在UIView的自定义属性
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
        // 实现了SDWebImageOperation的协议
            if ([operation respondsToSelector:@selector(cancel)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
objectivec 复制代码
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
    // 如果之前已经有过该图片的下载操作,则取消之前的图片下载操作
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}

实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。

通俗来说:这两个方法的组合使用,可以实现对图片加载操作的管理。在设置新的图片加载操作之前,会先取消之前的操作,从而确保每个视图只执行最新的图片加载操作。 这样可以避免出现重复加载或并发加载的问题,同时也提高了图片加载的效率和用户体验。

  • 重复加载: 指的是在同一时间点或短时间内多次加载相同资源的情况。这可能是由于代码逻辑错误、用户操作或系统问题导致的。重复加载可能会造成资源浪费、性能下降和不必要的网络请求。
  • 并发加载: 指的是在同一时间点或短时间内同时进行多个加载操作的情况。这种情况通常发生在多线程或并发执行的环境下,多个加载操作可以同时进行,以提高效率和响应性。但是必须确保每个加载操作独立处理自己的资源,否则会出现数据竞争和冲突。

占位图策略

作为图片下载完成之前的替代图片。dispatch_main_async_safe是一个宏,保证在主线程安全执行。

objectivec 复制代码
if (!(options & SDWebImageDelayPlaceholder)) {
	// 在主线程主队列中设置占位图
    dispatch_main_async_safe(^{
        // 设置占位图
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    });
}

判断url是否合法

如果url合法,则进行图片下载操作,否则直接block回调失败。

objectivec 复制代码
if (url) {
    // 下载图片操作
} else {
    dispatch_main_async_safe(^{
#if SD_UIKIT
        [self sd_removeActivityIndicator];
#endif
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            completedBlock(nil, error, SDImageCacheTypeNone, url);
        }
    });
}

下载图片操作

下载图片的操作是由SDWebImageManager完成的,它是一个单例。

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

下载完成之后刷新UIImageView的图片。

objectivec 复制代码
// 根据枚举类型,判断是否需要设置图片
shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                          (!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
    if (!sself) { return; }
    if (!shouldNotSetImage) {
    	// 设置图片
        [sself sd_setNeedsLayout];  
    }
    if (completedBlock && shouldCallCompletedBlock) {
        completedBlock(image, error, cacheType, url);
    }
};

// 不要自动设置图片,则调用block传入image对象
if (shouldNotSetImage) {    
    dispatch_main_async_safe(callCompletedBlockClojure);
    return;
}

// 设置图片操作
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();
});

最后,把返回的 operation 添加到operationDictionary中,方便后续的cancel。

objectivec 复制代码
// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

2. SDWebImageManager

SDWebImageManager的官方介绍:

  • SDWebImageManager是UIImageView+WebCache类别背后的类。
  • 它将异步下载程序(SDWebImageDownloader)与图像缓存存储(SDImageCache)绑定。
  • 您可以直接使用这个类,在另一个地方中使用缓存下载web图像,而不是一个UIView。

SDWebImageManager是隐藏在UIImageView+WebCache之后,用于处理异步下载和图片缓存的类。同时你也可以直接使用SDWebImageManager的如下方法来直接下载图片。

objectivec 复制代码
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;

下面我们看看SDWebImageManager.h里的内容。

定义了一些枚举类型的SDWebImageOptions。

objectivec 复制代码
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions ) {
    SDWebImageRetryFailed = 1 < < 0,
    SDWebImageLowPriority = 1 < < 1,
    SDWebImageCacheMemoryOnly = 1 < < 2,
    SDWebImageProgressiveDownload = 1 < < 3,
    SDWebImageRefreshCached = 1 < < 4,
    SDWebImageContinueInBackground = 1 < < 5,
    SDWebImageHandleCookies = 1 < < 6,
    SDWebImageAllowInvalidSSLCertificates = 1 < < 7,
    SDWebImageHighPriority = 1 < < 8,
    SDWebImageDelayPlaceholder = 1 < < 9,
    SDWebImageTransformAnimatedImage = 1 < < 10,
    SDWebImageAvoidAutoSetImage = 1 < < 11,
    SDWebImageScaleDownLargeImages = 1 < < 12,
};

表达了图片加载过程中对于图片的一些状态,如SDWebImageRetryFailed表示即使某个url下载失败了,SDWebImage还是会尝试再次下载它;SDWebImageLowPriority表示禁止图片在交互发生的时候下载(如滑动tableview)等等。

接下来声明了四个block。

objectivec 复制代码
// 操作完成的回调,被上层的扩展调用。
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

// 被SDWebImageManager调用。
// 如果使用了SDWebImageProgressiveDownload标记,这个block可能会被重复调用,直到图片完全下载结束,
// finished=true,再最后调用一次这个block。
typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);

// SDWebImageManager每次把URL转换为cache key的时候调用,可以删除一些image URL中的动态部分。
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL);

然后定义了SDWebImageManagerDelegate协议。

objectivec 复制代码
@protocol SDWebImageManagerDelegate 

@optional
// 控制在cache中没有找到image时 是否应该去下载。
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

// 在下载之后,缓存之前转换图片。在全局队列中操作,不阻塞主线程
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

SDWebImageManager是单例使用的,分别维护了一个SDImageCache实例和一个SDWebImageDownloader实例。其中包含一些方法:

objectivec 复制代码
// 初始化SDWebImageManager单例,在init方法中已经初始化了cache单例和downloader单例。
- (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader NS_DESIGNATED_INITIALIZER;
// 下载图片
- (id )downloadImageWithURL:(NSURL *)url
                    options:(SDWebImageOptions)options
                   progress:(SDWebImageDownloaderProgressBlock)progressBlock
                  completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
// 缓存给定URL的图片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
// 取消当前所有的操作
- (void)cancelAll;
// 监测当前是否有进行中的操作
- (BOOL)isRunning;
// 监测图片是否在缓存中, 先在memory cache里面找  再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
// 监测图片是否缓存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
// 监测图片是否在缓存中,监测结束后调用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
// 监测图片是否缓存在disk里,监测结束后调用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回给定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;

下面我们看看下载图片的主要过程,也就是下面这个方法:

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

判断url是否合法

objectivec 复制代码
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止应用程序在参数类型错误(如发送NSNull而不是NSURL)时崩溃
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

这里为了防止很多用户直接传递NSString,先将NSString类型url转换成NSURL类型,然后防止一下程序因参数类型错误导致的crash。

判断已加载失败的url

objectivec 复制代码
BOOL isFailedUrl = NO;
if (url) {  
	// 判断url是否是加载失败过的url
    LOCK(self.failedURLsLock);
    isFailedUrl = [self.failedURLs containsObject:url];
    UNLOCK(self.failedURLsLock);
}
// 如果url为空或者url下载失败并且设置了不再重试
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;
}

这里使用了集合failedURLs,用于保存之前加载失败的urls。如果url为空或者url之前失败过且有SDWebImageRetryFailed枚举类型,直接调用completedBlock返回错误。

保存操作

objectivec 复制代码
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);

使用可变数组runningOperations,用于保存所有的operation,并且监测是否有operation在执行,即判断running状态。

查找缓存

SDWebImageManager会首先在内存缓存和磁盘缓存的 cache 中查找是否下载过相同的照片,即调用imageCache的下面方法:

objectivec 复制代码
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

如果操作取消,则直接返回:

objectivec 复制代码
__strong __typeof(weakOperation) strongOperation = weakOperation;
// operation取消,那么将下载任务从下载队列中直接移除
if (!strongOperation || strongOperation.isCancelled) {
    [self safelyRemoveOperationFromRunning:strongOperation];
    return;
}

如果没有在缓存中找到图片,或者不管是否找到图片,只要operation有SDWebImageRefreshCached标记,那么若SDWebImageManagerDelegate的shouldDownloadImageForURL方法返回 true,即允许下载时,都使用 imageDownloader的下载方法:

objectivec 复制代码
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

如果下载有错误,直接调用completedBlock返回错误,并且视情况将url添加到failedURLs里面:

objectivec 复制代码
dispatch_main_sync_safe(^{
	// 下载有误,直接调用completedBlock返回错误
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
    }
});
//视error情况将url添加到failedURLs
if (error.code != NSURLErrorNotConnectedToInternet
 && error.code != NSURLErrorCancelled
 && error.code != NSURLErrorTimedOut
 && error.code != NSURLErrorInternationalRoamingOff
 && error.code != NSURLErrorDataNotAllowed
 && error.code != NSURLErrorCannotFindHost
 && error.code != NSURLErrorCannotConnectToHost) {
      @synchronized (self.failedURLs) {
          [self.failedURLs addObject:url];
      }
}

如果下载成功,若支持失败重试,将url从failURLs里删除:

objectivec 复制代码
if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
         [self.failedURLs removeObject:url];
    }
}

如果delegate中实现了imageManager:transformDownloadedImage:withURL:方法,图片在缓存之前,需要做转换(在全局队列中调用,不阻塞主线程)。转化成功后下载全部结束,图片存入缓存,调用completedBlock回调。其中第一个参数是转换后的image。

objectivec 复制代码
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];
        //缓存图片
        [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
    }
    dispatch_main_sync_safe(^{
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
        }
    });
});

存入缓存都是调用imageCache的下面方法:

objectivec 复制代码
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

如果没有在缓存找到图片,且不允许下载,直接调用completedBlock,第一个参数为nil。

objectivec 复制代码
dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !weakOperation.isCancelled) {
        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
    }
});

最后将这个operation从runningOperations里删除。

objectivec 复制代码
@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
 }

可以看到,SDWebImageManger负责处理和协调SDWebImageDownloader和SDWebImageCache,并与 UIKit层进行交互(传入图片)。

3. SDWebImageDownloader

SDWebImage的下载模块主要包括两个类:SDWebImageDownloader和SDWebImageDownloaderOperation。

SDWebImageDownloader负责对所有下载任务的管理,为下载图片提供专用和优化的异步下载器。

SDWebImageDownloaderOperation负责具体的一个下载任务的执行,且为了拓展下载功能,还支持实现SDWebImageDownloaderOperationInterface协议来自定义具体下载。

这里我们先看看SDWebImageDownloader。

SDWebImageDownloader基本属性

  • currentDownloadCount:显示当前仍然需要下载的大小
  • downloadTimeout:下载操作的超时时间(单位为秒)。默认值:15.0
  • executionOrder:修改下载操作的执行顺序。默认值为SDWebImageDownloaderFIFOExecutionOrder
  • headerFilter:设置过滤器用来挑选下载图片的HTTP请求的头。这个块在每个图片下载请求时被调用,返回在相应的HTTP请求中用于HTTP头的NSDictionary
  • maxConcurrentDownloads:最大的并发下载数
  • username:设置用户名
  • password:设置密码
  • sessionConfiguration:NSURLSession内部使用的配置
  • shouldDecompressImages:默认为YES,解压已经下载和缓存的图片可以提高性能,但是会消耗很多的内容。如果你遇到由于大量的消耗内存导致崩溃,建议设置为NO
  • urlCredential:为请求操作设置默认的URL证书
objectivec 复制代码
//显示当前仍然需要下载的大小
@property (readonly, nonatomic) NSUInteger currentDownloadCount;

//下载操作的超时时间(单位为秒)。默认值:15.0
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

//修改下载操作的执行顺序。默认值为SDWebImageDownloaderFIFOExecutionOrder
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

//设置过滤器用来挑选下载图片的HTTP请求的头。这个块在每个图片下载请求时被调用,返回在相应的HTTP请求中用于HTTP头的NSDictionary
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;

//最大的并发下载数
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

//设置用户名
@property (strong, nonatomic, nullable) NSString *username;

//设置密码
@property (strong, nonatomic, nullable) NSString *password;

//NSURLSession内部使用的配置
@property (readonly, nonatomic, nonnull) NSURLSessionConfiguration *sessionConfiguration;

//默认为YES,解压已经下载和缓存的图片可以提高性能,但是会消耗很多的内容。如果你遇到由于大量的消耗内存导致崩溃,建议设置为NO
@property (assign, nonatomic) BOOL shouldDecompressImages;

//为请求操作设置默认的URL证书
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;

SDWebImageDownloader枚举类型

SDWebImageDownloader枚举类型提供了很多下载选项,可以根据情况进行配置。如设置下载优先级、进度、后台下载,图片缩放等,同时支持先进先出,先进后出的下载方式。

objectivec 复制代码
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    //下载低优先级
    SDWebImageDownloaderLowPriority = 1 << 0,
    //下载高优先级
    SDWebImageDownloaderHighPriority = 1 << 7,
    // 带有进度
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    //默认不使用URLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    //如果图片是在NSURLCAche中读取时,调用completion block时,返回nil,配合SDWebImageDownloaderUseNSURLCache使用
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    //支持后台下载
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    //支持NSHTTPCookieStore的cookie信息,进而设置NSMutableURLRequest.HTTPShouldHandleCookies=YES
    SDWebImageDownloaderHandleCookies = 1 << 5,
    //允许不信任SSL证书
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    //缩放大图片
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
     //先进先出
    SDWebImageDownloaderFIFOExecutionOrder,
    //先进后出
    SDWebImageDownloaderLIFOExecutionOrder
};

核心方法

先看方法:

objectivec 复制代码
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    //如果URL为空,直接执行完成回调block并传入nil参数,结束本次请求
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
  //NSMutableDicitonary不是线程安全的,利用GCD的barrier 保证线程安全,确保字典不会同时存取
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        //为URL创建一个唯一对应的callbacks,并赋值给self.URLCallBacks
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

URLCallBacks字典存储的每个请求的callbacksForURL,走完上面这个函数,我们就能确保每个请求都能和它的progressBlock和completedBlock回调一一对应。

4. SDWebImageDownloaderOperation

前面说过,SDWebImageDownloaderOperation负责下载任务的具体实现,我们直接看核心方法。

核心方法

objectivec 复制代码
- (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;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 创建request,针对不同缓存策略不同处理
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        // 设置请求头部
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        // 创建操作对象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // 给操作对象设置urlCredential
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 设置操作级别
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        // 把操作添加到队列
        [sself.downloadQueue addOperation:operation];
        
        // 根据executionOrder设置,设置依赖
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            // 模拟后进先出执行顺序
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

取消某个操作

objectivec 复制代码
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    dispatch_barrier_async(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:token.url];
        }
    });
}

全部暂停或取消

objectivec 复制代码
- (void)setSuspended:(BOOL)suspended {
    (self.downloadQueue).suspended = suspended;
}

- (void)cancelAllDownloads {
    [self.downloadQueue cancelAllOperations];
}

重点问题

使用流程

  1. 入口setImageWithURL:placeholderImage:options:先显示预览图placeholderImage,然后 SDWebImageManager 根据URL开始处理图片
  2. SDWebImageManager调用downloadWithURL:delegate:options:userInfo:方法,进入SDImageCache的queryDiskCacheForKey:delegate:userInfo:方法,从缓存查找图片是否已经下载
  3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:到SDWebImageManager
  4. SDWebImageManagerDelegate回调webImageManager:didFinishWithImage:方法,以在UIImageView+WebCache,UIButton+WebCache等前端展示图片
  5. 如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存
  6. 根据URLKey在硬盘缓存目录下尝试读取图片文件。这一步在NSOperation进行,所以要回主线程进行结果回调:使用notifyDelegate:方法
  7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:,进而回调展示图片
  8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo:
  9. 共享或重新生成一个下载器SDWebImageDownloader开始下载图片。
  10. 图片下载由NSURLConnection来执行,实现相关delegate来判断图片下载中、下载完成和下载失败。
  11. connection:didReceiveData:中利用ImageIO提供了加载效果,展示图片下载进度。
  12. connectionDidFinishLoading:数据下载完成后交给SDWebImageDecoder做图片解码处理。
  13. 图片解码处理在一个NSOperationQueue完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  14. 在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:回调给SDWebImageDownloader。
  15. imageDownloader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成。
  16. 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片。
  17. 将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。
  18. SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
  19. SDWebImagePrefetcher可以预先下载图片,方便后续使用。

SDWebImage 的内存警告是如何处理

利用通知中心观察:

  • UIApplicationDidReceiveMemoryWarningNotification 监听内存警告通知,执行 clearMemory 方法,清理内存缓存。
  • UIApplicationWillTerminateNotification 监听应用程序将要终止通知,执行 cleanDisk 方法,清理磁盘缓存。
  • UIApplicationDidEnterBackgroundNotification 监听应用程序进入后台通知,执行 backgroundCleanDisk 方法,后台清理磁盘。

通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内。clearDisk 清空磁盘缓存表示:将所有缓存目录中的文件全部删除。实际情况中会将缓存目录直接删除,再次创建一个同名空目录。

其他小问题

  • Q:图片文件缓存的时间有多长?
    A:1周。static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
  • Q:SDWebImage 的内存缓存是用什么实现的?
    A:NSCache。SDImageCache类中有一个NSCache类型的memCache属性,用这个属性来进行内存缓存。
    存缓存:[self.memCache setObject:image forKey:key cost:cost];
    取缓存:return [imageFromMemoryCacheForKey:key];
  • Q:SDWebImage 的最大并发数是多少?
    A:maxConcurrentDownloads = 6为程序固定死了,其实可以通过属性进行调整。
  • Q:SDWebImage是如何区分不同格式的图像?
    A:根据图像数据第一个字节来判断的。
  • Q:SDWebImage 缓存图片的名称是怎么确定的?
    A:使用文件名保存,重名的几率很高,因此使用 MD5 的散列函数。对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串。
相关推荐
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__3 小时前
Web APIs学习 (操作DOM BOM)
学习
数据的世界015 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐6 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
恋猫de小郭8 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
OopspoO8 小时前
qcow2镜像大小压缩
学习·性能优化
A懿轩A8 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒8 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
kkflash39 小时前
提升专业素养的实用指南
学习·职场和发展
1 9 J9 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法