1. 前言
大名鼎鼎SDWebImage不用多说,相信每一个iOS程序员或多或少都有了解。比如我,之前就大概只知道是个什么东西,基本属于没用过的状态。最近抽空学习了一下源码,在此记录下。
在GitHub上,SDWebImage描述为Asynchronous image downloader with cache support as a UIImageView category,翻译成中文是“UIImageView的一个category,支持缓存的异步图片下载器”。
可以在该链接中查看到最新的文档https://sdwebimage.github.io
本文使用的源码为SDWebImage 5.0+版本:
2. 架构
在GitHub上,SDWebImage提供了非常详细的架构图、类图和顺序图,其中下图是整体的架构图

这个图中可以看到总体包括以下几个部分
- 基础组件:包括工具类、分类方法、Image Coder(图片编码/解码)、Image Transformer(图片转换)
-
顶层组件:
- Image Manager:负责处理Image Cache(处理图片缓存和落地)和Image Loader(处理图片的网络加载)
- View Category:提供对外的API接口,图片加载动画和转场动画等。
- Image Prefetcher:图片的预加载器,是相对比较独立的部分。
可以看到,SDWebImage提供了图片缓存的能力、网络加载的能力,还包括一些图片的处理。
顺序图

通过顺序图,可以清楚的看到整个接口的调用流程。
- 需要加载图片时,
Other Object只需要调用``UIImageView+WebCahce中的sd_setImageWithURL()`方法即可 -
sd_setImageWithURL()会调用UIVIew+WebCache中的内部加载方法sd_internalSetImageWithURL() - 接下来会调用
SDWebImageManager的loadImage()方法,可以看到,主要的逻辑都在这个SDWebImageManager中 -
SDWebImageManager会分别调用SDImageCache加载缓存数据,然后调用SDWebImageDownloader从网络中加载图片 - 加载完成后,会回调回
UIImageView中,设置图片
对于使用者来说,复杂的逻辑都隐藏在SDWebImageManager之后,还有一些更详细的类图,有兴趣的可以直接到GitHub的ReadMe去查看。
3. View Category
3.1 WebCache
SDWebImage提供了以下几个Category可以方便的完成图片加载
UIImageView+HighlightedWebCacheUIImageView+WebCacheUIButton+WebCacheNSButton+WebCacheUIView+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;
该方法非常长,主要的流程如下:
- 取消在进行的
operation。- 该
operation存储在由UIView+WebCacheOperation中维护的字典SDOperationsDictionary中,默认使用当前类名作为operation的key,其中value是weak指针,因为该operation由SDWebImageManager维护
- 该
- 若外部没有设置
SDWebImageDelayPlaceholder,则异步在主线程将placeholder设置到UIImageView中 - 重置记录进度的
NSProgress对象,该对象由当前分类实例维护 - 启动
ImageIndicator,默认是一个旋转菊花,其中iWatch是不支持的 - 接下来就是获取
SDWebImageManager了,可以支持外部配置,否则会使用全局唯一的单例 - 设置进度的回调
SDImageLoaderProgressBlock,该block中,会更新内部的进度条、菊花,然后再回调给外层调用者 - 调用
SDWebImageManager的加载方法loadImageWithURL:options:context:progress:completed:,启动图片的加载流程 - 在7中方法的
completed回调中,完成进度更新、关闭菊花、回调completedBlock以及设置图片等操作
4. SDWebImageManager
SDWebImageManager是一个单例类,维护了两个主要的对象imageCache和imageLoader:
@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;
4.1 加载图片前的准备工作
主要接口loadImageWithURL:options:context:progress:completed:的实现逻辑如下:
- 兼容逻辑,若传进来的url是
NSString而不是NSURL,则转换为NSURL - 创建一个新的
SDWebImageCombinedOperation - 判断是否是已经失败且不需要重试的url或者url无效,直接回调
completedBlock返回 - 将
operation加入到SetrunningOperations中 - 在执行加载操作前,调用
processedResultForURL方法,对url、options和context做一次加工操作- 在该方法中,
SDWebImageManager设置会判断是否外部有设置transformer、cacheKeyFilter和cacheSerializer, - 最后,会调用外部配置的
optionsProcessor对象的processedResultForURL方法,让使用者有机会修改上述参数
- 在该方法中,
- 调用
callCacheProcessForOperation方法,开始从缓存中加载图片
关键代码,代码中只保留关键逻辑:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 1
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 2
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// 3
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
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;
}
// 4
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);
// 5
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// 6
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
4.2 从缓存中加载图片
接口名称为callCacheProcessForOperation,该方法中
- 判断
context中是否传入了自定义的SDImageCache,否则使用默认的imageCache - 判断
options是否配置了SDWebImageFromLoaderOnly,该参数表明,是否仅从网络加载 - 若仅从网络中加载,直接调用
callDownloadProcessForOperation接口,开始下载的步骤 - 否则,获取url对应的
key,并调用imageCache的接口queryImageForKey,从缓存中加载图片,在该接口回调中,调用3中的下载接口。
关键代码如下:
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 1
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// 2
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// 4
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [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:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] 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 {
// 3
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
从网络中加载图片
接口名为callDownloadProcessForOperation,实现逻辑如下:
- 与SDImageCache类似,SDImageLoader也支持外部配置,否则使用默认的
imageLoader - 一系列参数判断,主要为了判断是否可以下载
- 当有图片时,在该方法中可能会先通过
callCompletionBlockForOperation接口,异步回调completedBlock设置已经加载好的图片 - 当判断可以下载后,会调用
imageLoader的requestImageWithURL接口,启动下载 - 在
requestImageWithURL的回调中,处理一些失败等异常逻辑。 - 若加载成功,则通过
callStoreCacheProcessForOperation接口,将下载的图片缓存到本地 - 当不需要下载时,会直接返回,若有缓存则会带上缓存的图片。
关键代码:
- (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 {
// 1
id<SDImageLoader> imageLoader;
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
imageLoader = context[SDWebImageContextImageLoader];
} else {
imageLoader = self.imageLoader;
}
// 2
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [imageLoader canRequestImageForURL:url];
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 3
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 将cachedImage传到image loader中用于检查是否是相同的图片
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
// 4
@weakify(operation);
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
// 5
if {
// 一系列失败逻辑
} else {
// 6
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
}];
} else if (cachedImage) { // 7
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
该方法中第6步从网络拉取成功后,会调用callStoreCacheProcessForOperation方法将图片缓存到本地,以及通过调用者提供的SDImageTransformer转换图片。
缓存图片
调用者提供两种自定义操作:
- 自定义的
SDImageTransformer将图片转换成另一个图片 - 自定义的
SDWebImageCacheSerializer将图片序列化为NSData
具体逻辑如下:
- 如果有提供
SDWebImageCacheSerializer,则会先调用接口将图片序列化之后,再调用存储接口缓存图片。注意这一步是放在global_queue中执行的,不会阻塞主线程,同时使用autoreleasepool保证NSData能第一时间释放。 - 第1步结束后,调用
storeImage接口,通过imageCache对象将图片缓存到本地。默认该操作是放在imageCache维护的io队列中执行的。 - 最后一步操作,则是调用
callTransformProcessForOperation接口,转换图片。
关键代码:
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 默认拉回来的图片就是originImage,当提供了transformer转化图片时,可以选择将原图片和转换后的图片都缓存起来
NSString *key = [self cacheKeyForURL:url context:context];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// 这里会缓存原图,如果转换只要完成下载,始终缓存原图
if (shouldCacheOriginal) {
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
// 1 放到全局队列中异步序列化
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
[self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
});
} else {
// 2
[self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
} else {
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
}
转换图片
如果外部有设置SDImageTransformer,则会判断是否需要将转换后的图片也缓存起来,关键代码:
- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
originalData:(nullable NSData *)originalData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// the target image store cache type
NSString *key = [self cacheKeyForURL:url context:context];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
if (transformedImage && finished) {
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
} else {
cacheData = (imageWasTransformed ? nil : originalData);
}
// keep the original image format and extended data
SDImageCopyAssociatedObject(originalImage, transformedImage);
[self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
});
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
加载完成
一切完成后,会通过callCompletionBlockForOperation回调到最外层的调用者。
5. SDImageCache
关于SDIMageCache,可以直接看以下类图,用协议定义了所有关键类,包括SDImageCache、SDMemoryCache、SDDiskCache。

5.1 SDImageCache
- 持有
SDMemoryCache和SDDiskCache,用于从内存和硬盘中加载图片。可以通过SDImageCacheConfig配置我们自定义实现的Cache类 - 维护了一个io队列,所有从硬盘中异步读取内容的操作均通过该io队列执行
- 监听了App进程被系统杀掉和App切换到后台的通知,清除过期的数据
获取图片接口queryCacheOperationForKey
首先判断外部是否有传入
transformer对象,若有,则会将key通过SDTransformedKeyForKey接口将key和tranformerKey拼接在一起得到新的key通过
memoryCache从内存中获取图片,默认情况下,如果获取到图片,则直接返回若设置了
SDImageCacheQueryMemoryData参数,则仍然从硬盘中加载图片的data数据。默认异步从硬盘加载,可通过设置参数同步加载加载完成后,通过
block同步或异步返回
有两处细节需要注意:
- 使用
@autoreleasepool保证大的内存占用可以快速释放 - 异步加载时,使用
io队列。异步回调block时,使用主线程回调
关键代码:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
// 1
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer)
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
// 处理SDImageCacheDecodeFirstFrameOnly或SDImageCacheMatchAnimatedImageClass的逻辑
}
// 2
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 3
NSOperation *operation = [NSOperation new];
// 检查是否需要同步查询disk
// 1. 内存缓存命中且设置了同步
// 2. 内存缓存没有命中但设置了同步读取硬盘数据
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
// 从硬盘中加载图片的data
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) { // 内存中已经有图片,但是需要图片data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// 将imageData转换成image
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
// 将图片缓存到内存中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// 4
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
存储图片接口storeImage
外部可设置不同的SDImageCacheType,决定是否需要缓存到内存以及硬盘中
内存缓存:根据
shouldCacheImagesInMemory接口判断是否要缓存到内存中-
硬盘缓存:
- 首次将图片转换为
NSData,使用SDAnimatedImage接口或者SDImageCodersManager将图片转化为NSData - 通过
diskCache存储NSData到硬盘中 - 检查图片是否有
sd_extendedObject,如果有则也会存储到硬盘中,使用了NSKeyedArchiver将sd_extendedObject转换为NSData-
NSKeyedArchiver的在iOS 11上提供了新的接口archivedDataWithRootObject:requiringSecureCoding:error - 这里为了兼容iOS 11以下的系统,使用了旧的接口
archivedDataWithRootObject:,通过clang diagnostic ignored "-Wincompatible-pointer-types"屏蔽了方法过期警告;使用try catch捕获异常 - 通过
diskCache的setExtendedData将扩展数据存储到硬盘中
-
- 首次将图片转换为
关键代码:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
// 1
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
// 2
if (toDisk) {
// 使用iO队列异步存储
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
// 2.1
NSData *data = imageData;
if (!data && image) {
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
// 2.2
[self _storeImageDataToDisk:data forKey:key];
if (image) {
// 2.3
id extendedObject = image.sd_extendedObject;
if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
NSData *extendedData;
// 2.3.1
if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
NSError *error;
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
if (error) {
NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
}
} else {
// 2.3.2
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
}
}
if (extendedData) { // 2.3.4
[self.diskCache setExtendedData:extendedData forKey:key];
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
5.2 SDMemoryCache
SDMemoryCache继承自NSCache,其内部做了如下一些事情:
持有一个NSMapTable的weakCache
该weakCache的key为strong类型,value为weak类型,在缓存图片时,weakCache也会缓存一份图片的key和value。
这么做的目的是,当NSCache因内存警告清除了缓存内容后,如果有图片在App某些地方仍然被引用,那么就可以通过weakCache来快速加入到NSCache中,从而阻止了重复从硬盘中读取。
weakCacheLock
使用了GCD的dispatch_semaphore_t信号量方式,保证多线程操作weakCache时的安全性。
5.3 SDDiskCache
SDDiskCache内部通过NSFileManager实现了文件的读写。值得注意的是
- 存储文件到硬盘时,
SDDiskCache会将存储的key转换成md5值后存入本地。 - 清理过期数据逻辑,总共分两个步骤
- 第一个步骤:根据
SDImageCacheConfigExpireType设定的排序依据,删除超过设定的过期时间的文件。 - 在遍历所有文件时,计算当前存储文件的总大小。
- 第二个步骤:当存储的总大小超过设定的总大小时,按照
SDImageCacheConfigExpireType设定的时间排序,删除文件,直到设定大小的1/2为止。 - 清除文件的时机是在App退出或退到后台时,由
SDImageCache调用。
- 第一个步骤:根据
- 存储
extendData:使用了系统的库<sys/xattr.h>,通过setxattr,getxattr,removexattr实现了extendData的设置、读取、移除操作。
清理过期数据的关键代码:
- (void)removeExpiredData {
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 删除过期的文件
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 存储文件属性为后边的文件大小检查做准备
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// 若剩余的文件大小仍然超过了设定的最大值,那么执行第二步步骤。优先删除更早的文件
NSUInteger maxDiskSize = self.config.maxDiskSize;
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
// 目标是删除到最大值的一半
const NSUInteger desiredCacheSize = maxDiskSize / 2;
// 按时间排序
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// 删除文件直到剩余大小是最大值的一半
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
}
6. SDImageLoader
SDImageLoader的类图如下,该模块主要处理网络请求逻辑。

6.1 SDWebImageDownloader
SDWebImageDownloader是SDWebImage提供的图片下载器类,实现了SDImageLoader协议。提供了一些配置参数以及多个下载图片接口。
@interface SDWebImageDownloader : NSObject
@property (nonatomic, copy, readonly) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderDecryptor> decryptor;
@property (nonatomic, readonly) NSURLSessionConfiguration *sessionConfiguration;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options context:(SDWebImageContext *)context progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
#pragma mark - Protocol<SDImageLoader>
- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock;
一些关键的参数如下:
downloadQueue:NSOperationQueue类型,用于执行每一个下载任务创建的NSOperation;URLOperations:字典类型,key为URL,value是NSOperation<SDWebImageDownloaderOperation>,使用该对象来维护SDWebImageDownloader生命周期内所有网络请求的Operation对象。session:使用外部或者默认的sessionConfiguration创建的NSURLSession对象。
图片下载核心流程
核心图片下载方法为downloadImageWithURL,主要流程如下:
判断
URLOperations是否已经缓存该url对应的NSOperation<SDWebImageDownloaderOperation>对象若已经存在
operation,将该方法传入的progressBlock和completedBlock加入到operation中,同时若该operation还未被执行时,会根据传入的options调整当前queue的优先级。若
operation不存在、已经完成或者被取消,通过createDownloaderOperationWithUrl方法创建一个新的operation。operation创建成功,设置completionBlock,添加operation到URLOperations中,调用addHandlersForProgress添加progressBlock和completedBlock,最后,将operation添加到downloadQueue(根据苹果文档,在添加operation到queue之前,需要执行完所有配置)。最后,创建并返回
SDWebImageDownloadToken对象,该对象包含了url、request、以及downloadOperationCancelToken。`
关键代码:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
id downloadOperationCancelToken;
// 1
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// 3
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
[self.URLOperations removeObjectForKey:url];
};
self.URLOperations[url] = operation;
// 4
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
[self.downloadQueue addOperation:operation];
} else { // 2
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
} else {
operation.queuePriority = NSOperationQueuePriorityNormal;
}
}
}
// 5
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
SDWebImageDownloadToken实现了SDWebImageOperation协议,对于外部调用者来说,可以通过id<SDWebImageOperation>取消当前操作,定义如下:
@interface SDWebImageDownloadToken : NSObject <SDWebImageOperation>
- (void)cancel;
@property (nonatomic, strong, nullable, readonly) NSURL *url;
@property (nonatomic, strong, nullable, readonly) NSURLRequest *request;
@property (nonatomic, strong, nullable, readonly) NSURLResponse *response;
@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics;
@end
创建NSOperation<SDWebImageDownloaderOperation>对象
NSOperation通过createDownloaderOperationWithUrl方法创建,主要流程如下:
- 创建
NSMutableURLRequest对象,设置缓存策略、是否使用默认Cookies 、Http头信息等。 - 获取外部配置的
SDWebImageDownloaderRequestModifier对象,若没有则使用self的,通过modifiedRequestWithRequest接口在请求之前有机会检查并修改一次Request,若返回了nil,本次请求会终止。 - 外部同样可以配置
SDWebImageDownloaderResponseModifier对象,用来修改Response,这个会先存储在context中,等待请求回来后再去调用。 - 获取
SDWebImageDownloaderDecryptor对象,同样是请求回来后,用于解密相关操作。 -
context参数检查完毕后,需要创建NSOperation<SDWebImageDownloaderOperation>对象,此处可以通过设置config的operationClass来传入自定义的类名,若外部没有传入,则会使用SDWebImage提供改的SDWebImageDownloaderOperation类。 - 设置http请求的证书,首先获取
config中的urlCredential,其次通过config中的usrname和password创建NSURLCredential对象。 - 设置其他参数,如http请求的证书、最小进度间隔、当前请求的优先级等。
- 如果设置了
SDWebImageDownloaderLIFOExecutionOrder,表明所有的请求都是LIFO(后进先出)的执行方式,此处的处理方式是遍历当前downloadQueue的operations,将新的operation设置为所有operations的依赖,代码如下:
关键代码:
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context {
NSTimeInterval timeoutInterval = self.config.downloadTimeout;
// 1
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; // 默认情况下不使用NSURLCache
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
// 2
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]];
} else {
request = [mutableRequest copy];
}
// 3
id<SDWebImageDownloaderResponseModifier> responseModifier;
if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
} else {
responseModifier = self.responseModifier;
}
if (responseModifier) {
mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
}
// 4
id<SDWebImageDownloaderDecryptor> decryptor;
if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
} else {
decryptor = self.decryptor;
}
if (decryptor) {
mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
}
context = [mutableContext copy];
// 5
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
// 6
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];
}
}
// 7
if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 8
if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
for (NSOperation *pendingOperation in self.downloadQueue.operations) {
[pendingOperation addDependency:operation];
}
}
return operation;
}
6.2 SDWebImageDownloaderOperation
在前边的SDWebImageDownloader初始化时,可以看到创建了NSURLSession对象,且delegate设置的为self,但实际上,当SDWebImageDownloader接收到NSURLSessionTaskDelegate或者NSURLSessionDataDelegate回调时,都会转发到对应的NSOperation<SDWebImageDownloaderOperation>对象去处理,默认情况,就是SDWebImageDownloaderOperation。来看看这里的主要流程吧。
启动方法start
通过
beginBackgroundTaskWithExpirationHandler方法申请在进入后台后,更多的时间执行下载任务。判断
session,该类中有两个session,unownedSession(外部传入),ownedSession(内部创建),当外部没有传入session时,内部则会再创建一个,保证任务可以继续执行。保存缓存数据,如果设置了
SDWebImageDownloaderIgnoreCachedResponse时,当拉取回来的数据和已缓存的数据一致,就回调上层nil,这里保存的缓存数据用于拉取结束后的判断。通过
dataTaskWithRequest创建NSURLSessionTask对象dataTask。设置
dataTask和coderQueue的优先级。启动本次任务,通过
progressBlock回调当前进度,这里block可以存储多个,外部通过addHandlersForProgress方法添加。这里还会再在主线程抛一个启动的通知
SDWebImageDownloadStartNotification。
关键代码
- (void)start {
// 1
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak typeof(self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
[wself cancel];
}];
}
#endif
// 2
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
// 3
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
// 4
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
// 5
if (self.dataTask) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
} else {
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
// 6
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__block typeof(self) strongSelf = self;
// 7
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
}
}
NSURLSessionTaskDelegate和NSURLSessionDataDelegate
在SDWebImageDownloaderOperation中,实现了NSURLSession的delegate回调处理,具体逻辑比较多且不复杂,就不在这里赘述,可自行查阅代码。
7. 一些细节
7.1 宏定义
多平台适配
SDWebImage中多处地方使用了平台宏去区分不同平台的特性,对于想要了解跨平台的一些特性,非常有借鉴意义。
// iOS and tvOS are very similar, UIKit exists on both platforms
// Note: watchOS also has UIKit, but it's very limited
#if TARGET_OS_IOS || TARGET_OS_TV
#define SD_UIKIT 1
#else
#define SD_UIKIT 0
#endif
#if TARGET_OS_IOS
#define SD_IOS 1
#else
#define SD_IOS 0
#endif
#if TARGET_OS_TV
#define SD_TV 1
#else
#define SD_TV 0
#endif
#if TARGET_OS_WATCH
#define SD_WATCH 1
#else
#define SD_WATCH 0
#endif
以及通过宏将Mac平台的NSImage声明为UIImage,NSImageView声明为UIImageView等,让一套代码得以方便你的适配多个平台不同的控件名称。
#if SD_MAC
#import <AppKit/AppKit.h>
#ifndef UIImage
#define UIImage NSImage
#endif
#ifndef UIImageView
#define UIImageView NSImageView
#endif
#ifndef UIView
#define UIView NSView
#endif
#ifndef UIColor
#define UIColor NSColor
#endif
#else
#if SD_UIKIT
#import <UIKit/UIKit.h>
#endif
#if SD_WATCH
#import <WatchKit/WatchKit.h>
#ifndef UIView
#define UIView WKInterfaceObject
#endif
#ifndef UIImageView
#define UIImageView WKInterfaceImage
#endif
#endif
#endif
判断是否主线程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
一般情况下,需要判断是否在主线程,可能会使用NSThread.isMainThread来判断,这个就可以满足大部分的场景了。而SDWebImage的实现有些不一样,判断的方式是当前的queue是否是主队列,并没有判断当前的线程。
实际上,主线程和主队列不完全是一个东西,有微小的区别。主线程上也可以运行其他队列。
在这篇OpenRadar中有提到,在主线程但非主队列中调用MKMapView的addOverlay方法是不安全的。具体可参考下列文章:
7.2 多线程安全
在代码中有大量的地方使用了锁去保证多线程安全,包括常见的@synchonzied以及GCD的信号量
#ifndef SD_LOCK
#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif
#ifndef SD_UNLOCK
#define SD_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif
8. 结语
SDWebImage代码暂时就讲解这么多,不过该库的功能远不止于此,非常强大,对于有需要使用的,可以再详细的去了解具体使用的地方。