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+HighlightedWebCache
UIImageView+WebCache
UIButton+WebCache
NSButton+WebCache
UIView+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 imageCache;
@property (strong, nonatomic, readonly, nonnull) id 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 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 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 transformer = context[SDWebImageContextImageTransformer];
id 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 transformer = context[SDWebImageContextImageTransformer];
id 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 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
:使用了系统的库
,通过setxattr
,getxattr
,removexattr
实现了extendData
的设置、读取、移除操作。
清理过期数据的关键代码:
- (void)removeExpiredData {
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *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 *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 *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 requestModifier;
@property (nonatomic, strong) id responseModifier;
@property (nonatomic, strong) id 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
- (id)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock;
一些关键的参数如下:
downloadQueue
:NSOperationQueue
类型,用于执行每一个下载任务创建的NSOperation
;URLOperations
:字典类型,key
为URL,value
是NSOperation
,使用该对象来维护SDWebImageDownloader
生命周期内所有网络请求的Operation
对象。session
:使用外部或者默认的sessionConfiguration
创建的NSURLSession
对象。
图片下载核心流程
核心图片下载方法为downloadImageWithURL
,主要流程如下:
判断
URLOperations
是否已经缓存该url
对应的NSOperation
对象若已经存在
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 *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
取消当前操作,定义如下:
@interface SDWebImageDownloadToken : NSObject
- (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
对象
NSOperation
通过createDownloaderOperationWithUrl
方法创建,主要流程如下:
- 创建
NSMutableURLRequest
对象,设置缓存策略、是否使用默认Cookies 、Http头信息等。 - 获取外部配置的
SDWebImageDownloaderRequestModifier
对象,若没有则使用self
的,通过modifiedRequestWithRequest
接口在请求之前有机会检查并修改一次Request
,若返回了nil
,本次请求会终止。 - 外部同样可以配置
SDWebImageDownloaderResponseModifier
对象,用来修改Response
,这个会先存储在context
中,等待请求回来后再去调用。 - 获取
SDWebImageDownloaderDecryptor
对象,同样是请求回来后,用于解密相关操作。 -
context
参数检查完毕后,需要创建NSOperation
对象,此处可以通过设置config
的operationClass
来传入自定义的类名,若外部没有传入,则会使用SDWebImage
提供改的SDWebImageDownloaderOperation
类。 - 设置http请求的证书,首先获取
config
中的urlCredential
,其次通过config
中的usrname
和password
创建NSURLCredential
对象。 - 设置其他参数,如http请求的证书、最小进度间隔、当前请求的优先级等。
- 如果设置了
SDWebImageDownloaderLIFOExecutionOrder
,表明所有的请求都是LIFO
(后进先出)的执行方式,此处的处理方式是遍历当前downloadQueue
的operations
,将新的operation
设置为所有operations
的依赖,代码如下:
关键代码:
- (nullable NSOperation *)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 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 responseModifier;
if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
} else {
responseModifier = self.responseModifier;
}
if (responseModifier) {
mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
}
// 4
id 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 *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
。来看看这里的主要流程吧。
启动方法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
#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
#endif
#if SD_WATCH
#import
#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
方法是不安全的。具体可参考下列文章:
- GCD's Main Queue vs. Main Thread
- iOS知识小集之main-queue!=main-thread
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
代码暂时就讲解这么多,不过该库的功能远不止于此,非常强大,对于有需要使用的,可以再详细的去了解具体使用的地方。