简介
SDWebImage
是目前使用最广泛的第三方图片处理库,它不仅能够异步加载网络图片,还提供了一套图片缓存管理机制,功能非常强大。我们使用只要一行代码就可以轻松的实现图片异步下载和缓存功能。
本文主要针对v4.0.0
版本进行分析, 从3.0
升级4.0
的小伙伴可以查看 SDWebImage 4.0迁移指南;
特性
- 提供
UIImageView
、UIButton
的分类, 支持网络图片的加载与缓存管理 - 一个异步的图片下载器
- 异步(内存+磁盘)缓存和自动过期处理缓存
- 后台图片解压缩
- 同一个 URL 不会重复下载
- 自动识别无效 URL,不会反复重试
- 不阻塞主线程
- 高性能
- 使用
GCD
和ARC
支持的图像格式
- 支持
UIImage(JPEG,PNG,...)
,包括GIF
(从4.0版本开始,依靠FLAnimatedImage来处理动画图像) -
WebP
格式,包括动画WebP
(使用WebPsubspec
)
使用
UIImageView+WebCache
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];;
这是UIImageView
的一个扩展方法,通过这个方法就已经实现了 图片异步加载
和缓存机制
功能了, 是不是超简单呢
加载流程
首先: 我们通过时序图
,来了解一下框架的基本加载流程~
- 当我们的
UIImageView
、UIButton
控件调用sd_setImageWithURL: ()...
方法 来进行加载图片; - 框架会直接调用
UIView+WebCache
中的sd_internalSetImageWithURL:() ...
, 该方法是UIImageView
和UIButton
的共有拓展方法 - 接下来调用
SDWebImageManager
类中的loadImageWithURL:() ...
方法,会根据提供的图片URL
加载图片,SDWebImageManager
主要负责管理SDImageCache
缓存和SDWebImageDownloader
下载器 - 首先进入
SDImageCache
类,调用queryCacheOperationForKey...
在内存或者磁盘进行查询,如果有图片缓存则进行回调展示, 如果没有查询到图片缓存,则进行下一步下载 - 在未查询到图片缓存时,
SDWebImageDownloader
类会进行网络下载,下载成功后进行回调展示,并将下载的图片缓存到内存和磁盘
通俗理解:
根据Url
在内存
中查询图片,如果有则展示,没有则在磁盘
查询图片,查询到展示, 没有查询到在会通过网络下载
进行展示。下载完后会存储到内存和磁盘
,方便下次直接使用,磁盘查询和网络下载都是异步的,不会影响主线程
.
源码分析
UIView+WebCache 加载逻辑
我们了解了基本的加载流程后,接下来我们看看他是如何一步一步实现的,首先我们来看一下
UIView+WebCache.m
文件中的加载图片源码:
/**
@param url 图片URL
@param placeholder 占位图
@param options 加载选项(SDWebImageOptions枚举类型)
@param operationKey 要用作操的key键。如果为nil,将使用类名
@param setImageBlock 自定义Block块
@param progressBlock 下载进度Block块
@param completedBlock 加载完成Block块
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 这个方法很强大,用于保障当前的加载操作,不会被之前的加载操作污染(会将之前的加载操作取消)
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 占位图设置
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
// 通过 SDWebImageManager类来进行图片加载
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
// 会判断是否是否在主线程,如果在则直接回调,不在则执行主线程异步回调
dispatch_main_async_safe(^{
if (!sself) {
return;
}
// 如果选项为 SDWebImageAvoidAutoSetImage(用户手动设置), 则Block回调.不会给ImageView赋值,由用户自己操作
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image)
{
// 判断是UIimageView还是UIButton给对应的控件设置图像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 刷新视图布局
[sself sd_setNeedsLayout];
} else {
// 在加载网络图片失败后,展示展位图
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 加载完成回调
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 将 operation添加到 operation字典中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
else
{
// url为nil 则直接进行错误回调提示
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
UIView+WebCache
为UIImageView
提供了方法扩展,上面方法作为加载图片的入口,逻辑也简单清晰;
视图每次再加载图片之前, 都会调用 sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
方法, 用于取消上一个URl的加载操作, (比如: TalbeViewCell
中的ImageView
一般都是复用的,如果快速滑动, 这个ImageView会加载很多次不同的URL, 这个方法可以保证展示和加载的URL地址是最新的,并且会将之前的操作取消);
这个方法是UIView+WebCacheOperation
类中提供的一个方法,我们看下具体实现
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// SDOperationsDictionary是一个字典类型, key是当前视图的字符串, value是视图加载的操作
SDOperationsDictionary *operationDictionary = [self operationDictionary];
// 通过key获取到加载操作,operations实际是一个 SDWebImageCombinedOperation类型,并遵守了<SDWebImageOperation>协议
id operations = operationDictionary[key];
// 将操作取消掉
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel]; //在协议方法中实现取消操作
}
// 并从字典中将操作移除
[operationDictionary removeObjectForKey:key];
}
}
每个视图内部会有一个SDOperationsDictionary
字典, key
是当前视图的字符串, value
是视图加载的操作, 这个操作类遵守<SDWebImageOperation>
协议, 在协议方法中实现真正的取消操作
dispatch_main_async_safe(^{})
宏定义
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
该宏可以判断是否是在主线程,如果在主线程则直接异步执行block, 如果不在则调用 dispatch_async(dispatch_get_main_queue(), block);
回主线程异步执行;
SDWebImageManager
SDWebImageManager
是一个单例管理类,主要用于管理SDWebImageDownloader
以及SDImageCache
;SDImageCache
类负责执行图片的缓存,SDWebImageDownloader
类负责图片的下载任务。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
我们看到他有个内部有一个 id<SDWebImageManagerDelegate>
delegate属性,SDWebImageManagerDelegate
声明如下:
// 缓存中没有图片的话是否下载这张图片,如果返回NO的话,则如果缓存中没有这张图片,也不会去重新下载
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
如果我们要设置 只加载缓存,不加载网络图片
,或者在拿到图片做一些处理
,可以遵守改协议,自己实现加载逻辑;
在SDWebImageManager.m
页面同样还有一个 SDWebImageCombinedOperation
类,并遵守<SDWebImageOperation>
协议,主要负责取消一些未执行的NSOperation操作和正在执行的操作
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end
协议方法实现:
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
这个就是文章一开始,每次视图加载前都需要取消之前的操作所执行的方法,通过cancelled
属性来中断正在进行的操作
下面我们来看下:SDWebImageManager
的loadImageWithURL: options: progress: completed...
加载图片源码具体实现:
/*
加载图片,返回缓存版本。 如果不在缓存中,则使用给的URL下载图像
*/
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 如果是NSString类型则进行强转NSurl
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止应用程序在参数类型错误上崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 自定义Operation对象(继承NSObject) 内部有一个 NSOperation对象
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
// 检查是否为Url是否是失效的
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// 如果url为空或者是失效的,则直接block回调,并附加上错误信息,不再进行加载操作
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
// 将当前操作 添加 到正在运行的操作 集合中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// url转字符串 Key
NSString *key = [self cacheKeyForURL:url];
// 并在缓存中查询 图片,如果内存中有,直接回调, 内存中没有,则会创建NSoperation操作,异步在 磁盘查询
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
// 判断操作是否被取消了
if (operation.isCancelled) {
// 从正在执行的操作中移除,并renturn操作
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 没有图片缓存 或者 options 为 SDWebImageRefreshCached ,或者代理设置需要网络加载,从网络刷新
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 如果有缓存,但是提供了SDWebImageRefreshCached,先展示之前的缓存
if (cachedImage && options & SDWebImageRefreshCached) {
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 根据SDWebImageOptions设置下载选项的 SDWebImageDownloaderOptions
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
//如果图像已经缓存但强制刷新,则强制关闭
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
//忽略从NSURLCache读取的图像,如果缓存了图像,则强制刷新
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 使用给定的URL创建SDWebImageDownloader加载器实例
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 如果Operation被取消
}
else if (error)
{
// 错误回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
// 加入黑名单
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else
{
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
// 是否磁盘缓存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// 如果图像被转换,传递nil,这样我们就可以重新计算图像中的数据
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
// 磁盘缓存
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
// 移除 Operation操作
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}
else if (cachedImage) // 从内存或者磁盘中查询到图片信息
{
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 进行Block回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 从正在运行的集合中移除 移除 operation操作
[self safelyRemoveOperationFromRunning:operation];
}
else
{
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
}
以上代码逻辑还是比较清晰的,方法内部会创建一个 SDWebImageCombinedOperation
对象,这个对象用于贯穿缓存查询和下载操作,如果operation.isCancelled
为YES
,则回调操作会直接 return
,不在继续执行;
方法内部首先会调用了[self.imageCache queryCacheOperationForKey:()...]
方法,用于查询图片缓存,如果没有缓存则会调用[self.imageDownloader downloadImageWithURL:()...]
下载方法, 在获得到图片后,会将图片通过Block回调传给视图。
SDImageCache缓存类
SDImageCache
是一个全局单例类, 负责处理内存缓存及磁盘缓存。其中磁盘缓存的读写操作是异步的,这样就不会对UI操作造成影响。在SDImageCache
内部有一个NSCache
属性,用于处理内存缓存, NSCache
是一个类似NSDictionary一个可变的集合,当内存警告时内部自动清理部分缓存数据。磁盘缓存的处理则是使用NSFileManager
对象来实现的。图片存储的位置是位于Cache
文件夹。另外,SDImageCache
还定义了一个串行队列
,来异步存储图片。
内存缓存与磁盘缓存相关变量的声明及定义如下:
@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
@end
@implementation SDImageCache {
NSFileManager *_fileManager;
}
// 初始化
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
我们看下SDImageCache
查询缓存源码:
// 异步查询缓存并在完成时调用完成的操作。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// key是空的则直接回调,并返回nil
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先检查内存缓存…
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果内存中有图片
if (image) {
NSData *diskData = nil;
// 判断是否为gif图
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
// 回调内存图片信息
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
// 内存未查到图片信息 则创建一个 operation
NSOperation *operation = [NSOperation new];
// 在一个串行里异步执行
dispatch_async(self.ioQueue, ^{
// 如果取消了,则进行回调
if (operation.isCancelled) {
return;
}
// 从磁盘进行查询
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
// 判断是否有图片并需要内存缓存
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 内存缓存一份
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 进行回调
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
return operation;
}
通过上面的代码,我们可以看出, 首先在内存中查询图片通过imageFromMemoryCacheForKey:() ...
// 从内存中查看是否有改图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
key
为图片的 URL 地址, Value
则是缓存图片,如果内存没有缓存,则会调用[self diskImageDataBySearchingAllPathsForKey:key]
进行磁盘查询
// 磁盘查询
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
if (data) {
return data;
}
NSArray<NSString *> *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
if (imageData) {
return imageData;
}
}
return nil;
}
如果内存和磁盘都未查询到图片,则会进行网络请求下载图片
另外SDImageCache
还提供了存储图片
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// 内存缓存
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 如果磁盘缓存
if (toDisk) {
dispatch_async(self.ioQueue, ^{ // 在一个串行队列里面进行
NSData *data = imageData;
if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; // 获取图片格式
data = [image sd_imageDataAsFormat:imageFormatFromData]; //根据格式返回二进制数据
}
[self storeImageDataToDisk:data forKey:key]; // 存储到磁盘
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task
放到串行队列中处理。在方法中,会先检测图片是PNG
还是JPEG
,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5后的串)。
SDWebImageDownloader下载管理器
SDWebImageDownloader
下载管理器是一个单例类,它主要负责图片的下载的管理。实质的下载操作则是通过SDWebImageDownloaderOperation
类继承NSOperation
,图片的下载操作是放在一个NSOperationQueue
操作队列中来完成的。
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader
类的maxConcurrentDownloads
属性来修改。
我看来看下downloadImageWithURL:()..options:(): progress():completed:()
下载操作内部实现
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
// 默认15秒
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 为了防止潜在的重复缓存(NSURLCache + SDImageCache),如果被告知其他信息,我们将禁用图像请求的缓存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
// 创建下载操作
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
// 是否解码
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 操作加入队列
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
该方法的内部会调用addProgressCallback..
方法,会将图片下载的一些回调信息存储在SDWebImageDownloaderOperation
类的URLCallbacks
属性中,该属性是一个字典,key
是图片的URL地址
,value
则是一个数组,包含每个图片的多组回调信息。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
// URL将用作回调字典的键,因此它不能为nil。如果是nil,立即调用完整的块,没有图像或数据。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
// 按顺序依次执行
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
// 创建下载 SDWebImageDownloaderOperation操作
operation = createCallback();
// 添加到URLOperations操作缓存中
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
// 下载完移除下载操作
[self.URLOperations removeObjectForKey:url];
};
};
}
// 将一些回到信息放入`SDWebImageDownloaderOperation`类的`callbackBlocks`属性中,并创建 downloadOperationCancelToken实例
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
在SDWebImageDownloaderOperation
对象加入到操作队列后,就开始调用该对象的start
方法,代码如下
// SDWebImageDownloaderOperation.m
- (void)start {
// 如果操作被取消,就reset设置
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
...
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
// 创建session的配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
// 创建session对象
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
// 开始下载任务
[self.dataTask resume];
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
// 创建任务失败
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
...
}
在下载过程中,会涉及鉴权、响应的statusCode
判断(404
、304
等等),以及收到数据后的进度回调等等,在最后的URLSession:task:didCompleteWithError
里做最后的处理,然后回调完成的block
,下面仅分析一下- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
的方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
...
if (error) {
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
// 如果options是忽略缓存,而图片又是从缓存中取的,就给回调传入nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
// 缓存图片
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 跳转图片的大小
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
// 不是Gif图像
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
// 下载是图片大小的0
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
// 把下载的图片作为参数回调
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
...
}