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
提供了图片缓存的能力、网络加载的能力,还包括一些图片的处理。
顺序图
SDWebImageSequenceDiagram.jpg通过顺序图,可以清楚的看到整个接口的调用流程。
- 需要加载图片时,
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<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
代码暂时就讲解这么多,不过该库的功能远不止于此,非常强大,对于有需要使用的,可以再详细的去了解具体使用的地方。
网友评论