SDWebImage
一个支持远程服务器图片加载缓存的库
功能简介
-
UIImageView
,UIButton
,MKAnnotationView
添加Web图像和缓存管理的类别 - 一个异步图片下载器
- 一个异步内存磁盘图片缓存且自动处理过期图片
- 背景图片压缩
- 保证同一个URL不会被多次下载
- 保证不会一次又一次地重试伪造的URL
- 保证主线程永远不会被阻塞
- 性能!
- 使用GCD和ARC
工作流程
- 入口
sd_setImageWithURL:placeholderImage:options:progress:completed:
会先取消上次的加载操作,再设置 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。 - 进入 **SDWebImageManager ** 中的
loadImageWithURL:options:progress:completed:
,交给 SDImageCache 从缓存中查找图片 - 先从内存缓存查找是否有图片
imageFromMemoryCacheForKey:
,如果内存中已经有图片缓存,直接调用 SDCacheQueryCompletedBlock。 - SDWebImageManager 回调 SDInternalCompletionBlock 到 UIView+WebCache 等前端展示图片。
- 如果内存缓存中没有,生成
queryDiskBlock
添加到队列中开始从硬盘查找图片。 - 根据哈希之后的 URL Key 在磁盘缓存目录下查找图片,这一步是根据 SDImageCacheOptions 决定同步查找还是异步在 ioQueue 队列中查找,查找完成后将图片添加到内存缓存中,然后异步回到主线程中再返回图片给 SDWebImageManager。
- 如果缓存中获取不到图片,则通过 SDWebImageDownloader 下载图片。
- 如果该URL已存在下载操作 NSOperation<SDWebImageDownloaderOperationInterface> operation (默认为 SDWebImageDownloaderOperation 类型),则将当前所对应的 progressBlock 和 completedBlock 添加到该 operation 的 callbackBlocks 数组中,图片下载由 **NSURLSession ** 来做。
- 在 SDWebImageDownloaderOperation 中的
URLSession:dataTask:didReceiveData:
中实现边下载边解码图片 - 下载完图片之后,遍历 callbackBlocks 数组中的所有完成回调操作,将下载到的二进制数据和图片返回给 SDWebImageManager,SDWebImageManager 将图片添加到缓存中。
源码分析
Cache
减少网络请求次数,节省流量,下载完图片后存储到本地,下载再获取同一个URL时,优先从本地获取,提升用户体验。
SDWebImage 对图片进行缓存工作主要由 SDImageCache 完成。主要用于处理内存缓存和磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。
内存缓存
内存缓存采用的是 NSCache + NSMapTable 双重缓存机制,SDMemoryCache 继承于 NSCache, 会自动处理内存缓存问题,并在收到内存警告的时候,移除自身所缓存的内存资源。但是SDMemoryCache 中的 weakCache 并不会在收到内存警告的时候清除。
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
weakCache 中存储的 key
值是对URLKey 的强引用,而 value
则是对 UIImage 的弱引用,并不会额外占用内存资源。
磁盘缓存
磁盘缓存的处理通过 NSFileManager 对象实现,图片存储的位置位于 cache 文件夹,还可以设置 customPaths 数组来自定义磁盘查询目录。另外 SDImageCache 中还有 ioQueue 串行队列来异步查询存储图片。
存图片
存储图片API:
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;
先保存到内存缓存中,同时保存对这张图片的一个弱引用
/// SDMemoryCache
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
// Store weak cache
LOCK(self.weakCacheLock);
// Do the real copy of the key and only let NSMapTable manage the key's lifetime
// Fixes issue #2507 https://github.com/SDWebImage/SDWebImage/issues/2507
[self.weakCache setObject:obj forKey:[[key mutableCopy] copy]];
UNLOCK(self.weakCacheLock);
}
}
接着异步缓存图片到磁盘中,根据图片类型,通过 SDWebImageCodersManager 将图片解码为 NSData 类型,将图片资源保存到默认的缓存目录中,文件名为对 key 进行 MD5 后的值。
查图片
查询图片API:
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; // 内存缓存中查
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key; // 磁盘缓存中查
从 SDImageCache 中查图片:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
优先从内存缓存中查找图片,默认内存中查到后不会从磁盘中查,内存查不到缓存,则从默认缓存目录和自定义的查找目录 customPaths 中遍历查找。
删图片
删除图片API:
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
同步从内存缓存中删除图片,同时对图片的弱引用也会删除,磁盘图片则是异步删除,磁盘图片资源只会从默认缓存目录中删除,而不会删除 customPaths 中的图片资源。
清缓存
清除缓存API:
- (void)clearMemory;
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;
删除磁盘缓存是异步删除。
在 iOS 应用或 TV 应用上,对于一些过期失效的磁盘资源,SDImageCache 会在合适的时机去清除:
- APP 即将销毁
- APP 已经进入后台
小结
NSCache + NSMapTable 双重缓存机制可保证 SDWebImage 内部缓存在收到内存警告而释放资源后,还能更快速的从当前 APP 的其他地方获取到这张图片。customPaths 传入的目录数组仅用于读操作,默认所有的 io 操作都在 ioQueue 中串行执行。
Downloader
SDWebImageDownloader
图片下载管理器,管理每个图片下载操作,并控制其生命周期。
- 所有的图片下载操作都放在 NSOperationQueue 并发操作队列 downloadQueue 中,最大并发数为6
- 每个 URL 所对应的下载操作都放在 URLOperations 中,当 URLOperations 不存在该 URL 所对应的下载操作
id<SDWebImageDownloaderOperationInterface>
时,才创建新的下载操作 SDWebImageDownloaderOperation 对象,并存入 URLOperations 中,如果已存在 SDWebImageDownloaderOperation 对象operation,则将 progressBlock 和 completedBlock 保存到 SDWebImageDownloaderOperation 对象的 callbackBlocks 回调数组中 - 作为 NSURLSession 和 NSURLSessionDataTask 代理
- 下载操作队列默认采用 FIFO 先进先出,可设置为 LIFO 后进先出
- 返回 SDWebImageDownloadToken 对象作为下载操作对象,多次调用 URL 的下载,返回的 SDWebImageDownloadToken 不同,但其属性 downloadOperation 却是同一个下载操作对象
SDWebImageDownloaderOperation
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
- (BOOL)cancel:(nullable id)token {
BOOL shouldCancel = NO;
LOCK(self.callbacksLock);
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
UNLOCK(self.callbacksLock);
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
处理 URL 对应的具体下载操作,自主管理下载操作状态。callbackBlocks 可变数组存储每一个回 SDWebImageDownloaderProgressBlock 和 SDWebImageDownloaderCompletedBlock 回调。
取消下载操作的时候,只是将想要取消的操作所对应的 token (即 SDWebImageDownloaderProgressBlock 和 SDWebImageDownloaderCompletedBlock 的键值对) 从 callbackBlocks 数组中移除。
当可变数组 callbackBlocks 中的回调数为0的时候,才会取消本次下载操作。
设置 option 为 SDWebImageDownloaderProgressiveDownload 可边下载边回调,正常则在图片下载完成后,在callCompletionBlocksWithImage:imageData:error:finished:
中回调图片数据:
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
}
});
}
如果存在多个回调,则按照添加的顺序回调的。
小结
对同一个 URL 的多次下载操作,只会生成一个 operation 对象,只有当 URL 无对应回调时,才会真正取消该下载操作
主体 Utils
SDWebImageCombinedOperation
内存缓存查询操作和图片下载操作的结合体,即包含了 SDWebImage 框架获取一张图片的2个主要耗时操作。
-
NSOperation *cacheOperation
耗时的磁盘查询操作, -
SDWebImageDownloadToken *downloadToken
网络图片下载操作
SDWebImageManager
在我们的平时使用中,很少直接操作SDWebImageDownloader和SDImageCache去下载保存图片,大都是通过SDWebImageManager来管理,即使通过UIImageView+WebCache等分类加载图片,最后也会使用SDWebImageManager来处理。
加载图片的方法为 loadImageWithURL:options:progress:completed:
判断 URL 的长度是否大于0;URL 是否在 failedURLs 集合中(存放网络资源异常的 URL 集合),若在,options 是否包含 SDWebImageRetryFailed
创建一个 SDWebImageCombinedOperation 对象,保存在 runningOperations 集合中;
从内存缓存 SDImageCache 中查询图片,并将返回的 NSOperation 赋值给 SDWebImageCombinedOperation 对象的 cacheOperation 属性
当没缓存图片或要求刷新数据的时候,通过 SDWebImageDownloader 下载图片,并将返回的 SDWebImageDownloadToken 对象赋值给 SDWebImageCombinedOperation 对象的 downloadToken 属性
SDWebImagePrefetcher
批预下载管理器,提前下载一批 URLs 所对应的图片,每次只能处理一批图片组。使用的图片管理器并不是 SDWebImageManager 单例,而且单独创建的实例对象;可设置最大并发数,默认为3。
主要用于提前下载图片数据,不依赖于 UI 层。
SDWebImageTransition
设置图片的过渡效果
Decoder
图片解码,讲图片二进制数据 NSData 解码出 UIImage,或将 UIImage 编码成 NSData
SDWebImageCodersManager
图片编解码管理器,可通过 addCoder:
和 removeCoder:
添加或移除解码器,coders 可变数组用于存放当前的所有解码器。默认只有 SDWebImageImageIOCoder 解码器
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);
for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canEncodeToFormat:format]) {
return YES;
}
}
return NO;
}
通过 SDWebImageCodersManager 编解码图片的时候,根据图片的二进制数据的第一个字节,获取图片格式类型,逆遍历 coders 中的所有解码器,直到遇到可以成功解码该格式的解码器为止。
SDWebImageImageIOCoder
支持 PNG, JPEG, TIFF 格式,同时也支持 GIF 格式,但是只会解码出第一帧的图片
SDWebImageGIFCoder
GIF 格式的专用解码器,通过 CGImage 遍历解码出 GIF 动图
SDWebImageWebPCoder
WebP 格式的专用解码器,若想解码出 WebP 格式的图片,需要单独导入 WebP 相关的库 pod 'SDWebImage/WebP'
小结
在 SDWebImage 4.0.0 之前,是可以直接设置 GIF 动图的,但是在 4.0.0 之后,加载的 GIF 动图只显示第一帧的图像。有2种方式显示网络上的 GIF 动图:
- 调用 SDWebImageCodersManager 的
addCoder:
方法注册 SDWebImageGIFCoder 解码器 - 再单独导入 FLAnimatedImage 库
pod 'SDWebImage/GIF'
,用 FLAnimatedImageView 替换 UIImageView
第二种方法的性能比第一种高
WebCache Categories
给 UIImageView,UIButton,NSButton,MKAnnotationView 等常用图片容易扩充异步图片加载方法。
UIImageView+WebCache 采用 UIView+WebCache 默认的赋值方式(统一当成 UIImageView 处理);而 UIButton+WebCache 则自己实现了 setImageBlock
,MKAnnotationView+WebCache 也是自己实现了 setImageBlock
UIView+WebCache
UIImageView,UIButton,MKAnnotationView 三个类的分类最后也是调用到了 UIView+WebCache 的sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context:
方法上:
- 根据 operationKey (默认为对象类名)先将上次的加载图片操作
id<SDWebImageOperation> operation
取消 - 设置占位图片
- 通过 SDWebImageManager 图片管理器加载图片
- 将 SDWebImageManager 返回的
id <SDWebImageOperation> operation
和当前操作符 operationKey 绑定保存在 sd_operationDictionary 可变哈希映射表中
UIImageView+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
对应的操作符 operationKey 为类名,而setImageBlock
也是 UIView+WebCache 默认以 UIImageView 处理
iOS 应用和 TV 应用
- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs {
[self sd_cancelCurrentAnimationImagesLoad];
NSPointerArray *operationsArray = [self sd_animationOperationArray];
[arrayOfURLs enumerateObjectsUsingBlock:^(NSURL *logoImageURL, NSUInteger idx, BOOL * _Nonnull stop) {
__weak __typeof(self) wself = self;
id <SDWebImageOperation> operation = [[SDWebImageManager sharedManager] loadImageWithURL:logoImageURL options:0 progress:nil completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong typeof(wself) sself = wself;
if (!sself) return;
dispatch_main_async_safe(^{
[sself stopAnimating];
if (sself && image) {
NSMutableArray<UIImage *> *currentImages = [[sself animationImages] mutableCopy];
if (!currentImages) {
currentImages = [[NSMutableArray alloc] init];
}
// We know what index objects should be at when they are returned so
// we will put the object at the index, filling any empty indexes
// with the image that was returned too "early". These images will
// be overwritten. (does not require additional sorting datastructure)
while ([currentImages count] < idx) {
[currentImages addObject:image];
}
currentImages[idx] = image;
sself.animationImages = currentImages;
[sself setNeedsLayout];
}
[sself startAnimating];
});
}];
@synchronized (self) {
[operationsArray addPointer:(__bridge void *)(operation)];
}
}];
}
static char animationLoadOperationKey;
// element is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
- (NSPointerArray *)sd_animationOperationArray {
@synchronized(self) {
NSPointerArray *operationsArray = objc_getAssociatedObject(self, &animationLoadOperationKey);
if (operationsArray) {
return operationsArray;
}
operationsArray = [NSPointerArray weakObjectsPointerArray];
objc_setAssociatedObject(self, &animationLoadOperationKey, operationsArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operationsArray;
}
}
- (void)sd_cancelCurrentAnimationImagesLoad {
NSPointerArray *operationsArray = [self sd_animationOperationArray];
if (operationsArray) {
@synchronized (self) {
for (id operation in operationsArray) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
}
operationsArray.count = 0;
}
}
}
对于这两个平台的应用,对 UIImageView 额外新增图片组的异步加载方法
UIImageView+HighlightedWebCache
- (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:nil
options:options
operationKey:@"UIImageViewImageOperationHighlighted"
setImageBlock:^(UIImage *image, NSData *imageData) {
weakSelf.highlightedImage = image;
}
progress:progressBlock
completed:completedBlock];
}
对应的操作符 operationKey 为 UIImageViewImageOperationHighlighted,而setImageBlock
则是自定义
UIButton+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock {
if (!url) {
[self.sd_imageURLStorage removeObjectForKey:imageURLKeyForState(state)];
} else {
self.sd_imageURLStorage[imageURLKeyForState(state)] = url;
}
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:imageOperationKeyForState(state)
setImageBlock:^(UIImage *image, NSData *imageData) {
[weakSelf setImage:image forState:state];
}
progress:nil
completed:completedBlock];
}
- (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock {
if (!url) {
[self.sd_imageURLStorage removeObjectForKey:backgroundImageURLKeyForState(state)];
} else {
self.sd_imageURLStorage[backgroundImageURLKeyForState(state)] = url;
}
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:backgroundImageOperationKeyForState(state)
setImageBlock:^(UIImage *image, NSData *imageData) {
[weakSelf setBackgroundImage:image forState:state];
}
progress:nil
completed:completedBlock];
}
UIButton 的 image 和 backgroundImage 所对应的 operationKey 根据不同的状态 state 而不同,setImageBlock
也不一样
Other
MKAnnotationView 的做法和 UIImageView 基本一致的,而 NSButton 则是和 UIButton 基本一致。
End
本文是对 SDWebImage 简单用法所涉及到的类进行一些简单的源码解析。这次的分析是基于 4.3.0 的解析