SDWebImage概述
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
1.一个异步的图片加载器。
2.一个异步的内存+磁盘图片缓存
3.支持GIF、WebP图片
4.后台图片解压缩处理
5.确保同一个URL的图片不被多次下载
6.确保非法的URL不会被反复加载
7.确保下载及缓存时,主线程不被阻塞。
这个框架的核心类是SDWebImageManger
,在外部有UIImageView+WebCache
和 UIButton+WebCache
为下载图片的操作提供接口。内部有SDWebImageManger
负责处理和协调 SDWebImageDownloader 和 SDWebImageCache:SDWebImageDownloader
负责具体的下载任务,SDWebImageCache负责关于缓存的工作:添加,删除,查询缓存。
SDWebImage大致流程如下图所示:
从这个流程图里可以大致看出,该框架分为两个层:UIKit层(负责接收下载参数)和工具层(负责下载操作和缓存)。
UIKit层
该框架最外层的类是UIImageView +WebCache,我们将图片的URL,占位图片直接给这个类。下面是这个类的公共接口:
@implementation UIImageView (WebCache)
- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}
- (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];
}
- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
[self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];
}
点击这些方法,最后都会走到:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
继续点击这个方法,可以看到这个方法调用"UIView+WebCache"
的方法:
- 为什么不是UIImageView+WebCache而要上一层到UIView的分类里呢? 因为SDWebImage框架也支持UIButton的下载图片等方法,所以需要在它们的父类:UIView里面统一一个下载方法。
- (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 {
return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil];
}
这里看一下这个方法的实现:
- 第一步:取消当前正在进行的异步下载,确保每个
UIImageView
对象中永远只存在一个operation
,当前只允许一个图片网络请求,该operation
负责从缓存中获取image
或者是重新下载image
。具体执行代码是:
// 每个UIImageView对象只有一个operation
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 取消之前的下载任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
看下这个取消下载方法,定义在UIView+WebCacheOperation
类中:
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
if (key) {
// 如果之前执行了下载任务,取消之前的下载任务。
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized (self) {
[operationDictionary setObject:operation forKey:key];
}
}
}
}
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
// 这里就是一个字典存储了operation
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];// 获取添加在UIView的自定义属性
id<SDWebImageOperation> operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。
为什么不直接在UIImageView+WebCache里直接关联这个对象呢?我觉得这里作者应该是遵从面向对象的单一职责原则(SRP:Single responsibility principle),就连类都要履行这个职责,何况分类呢?这里作者专门创造一个分类UIView+WebCacheOperation来管理操作缓存(字典)。
- 第二步:占位图策略
作为图片下载完成之前的替代图片。dispatch_main_async_safe:
保证在主线程安全执行。
// 关联对象动态添加属性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 获取上下文保存的调度组
dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];
// 判断是否设置了SDWebImageDelayPlaceholder这个选项,如果设置了则等到图片加载失败再设置占位图。
if (!(options & SDWebImageDelayPlaceholder)) {
if (group) {
// 进入调度组
// 源码只有dispatch_group_enter,没有dispatch_group_leave,这个可能要我们自己处理
dispatch_group_enter(group);
}
dispatch_main_async_safe(^{
// 设置占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
cacheType
:图片缓存类型
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
* The image wasn't available the SDWebImage caches, but was downloaded from the web.
*图片无法从SDWebImage缓存中获得,但已从网络下载
*/
SDImageCacheTypeNone,
/**
* The image was obtained from the disk cache.
* 从磁盘缓存中获取镜像。
*/
SDImageCacheTypeDisk,
/**
* The image was obtained from the memory cache.
* 从内存缓存中获取图像
*/
SDImageCacheTypeMemory
};
- 第三步:判断url是否合法
如果url合法,则进行图片下载操作,否则直接block回调失败
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
// 是否显示进度条
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
#endif
// reset the progress
// 初始化图片加载进度
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
// 获取SDWebImageManager
// 可以通过SDWebImageExternalCustomManagerKey设置自定义的SDWebImageManager,不然就使用默认的。
SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
// 设置图片加载进度回调
// 这里把外部传进来的progressBlock包了一层,先更新UIImageView的sd_imageProgress,再继续往下传。
__weak __typeof(self)wself = self;
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
// 创建SDWebImageOperation并保存到sd_setImageLoadOperation
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
//self是否被释放
if (!sself) {
return; }
#if SD_UIKIT
// 移除进度条
[sself sd_removeActivityIndicator];
#endif
// if the progress not been updated, mark it to complete state
// 如果进度没有更新,将其标记为完成状态
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
// 设置图片加载失败的回调block
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) {
return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
// case 1a:我们得到了一个图像,但是SDWebImageAvoidAutoSetImage标志被设置了// OR / case 1b:我们没有得到图像,SDWebImageDelayPlaceholder没有设置
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
// 我们得到了一个图像,SDWebImageAvoidAutoSetImage没有设置
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
// 我们没有图像,并且设置了SDWebImageDelayPlaceholder标志
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
// 检查是否使用图像过渡
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
if (group) {
dispatch_group_enter(group);
}
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
if (group) {
// compatible code for FLAnimatedImage, because we assume completedBlock called after image was set. This will be removed in 5.x
BOOL shouldUseGroup = [objc_getAssociatedObject(group, &SDWebImageInternalSetImageGroupKey) boolValue];
if (shouldUseGroup) {
dispatch_group_notify(group, dispatch_get_main_queue(), callCompletedBlockClojure);
} else {
callCompletedBlockClojure();
}
} else {
callCompletedBlockClojure();
}
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
// 回调用失败
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{
NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
工具层
SDWebImageManager
SDWebImageManager
同时管理SDImageCache
和SDWebImageDownloader
两个类,它是这一层的老大哥。在下载任务开始的时候,SDWebImageManager
首先访问SDImageCache
来查询是否存在缓存,如果有缓存,直接返回缓存的图片。如果没有缓存,就命令SDWebImageDownloader
来下载图片,下载成功后,存入缓存,显示图片。以上是SDWebImageManager
大致的工作流程。
看一下SDWebImageManager
的几个重要属性。
/* 它将异步下载器(SDWebImageDownloader)与图像缓存存储(SDImageCache)绑定。
你可以使用这个类直接受益于web图像下载与缓存在另一个上下文,而不是UIView。*/
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;//管理缓存
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader //下载器*imageDownloader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;//记录失效url的名单
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;//记录当前正在执行的操作
加载图片
SDWebImageManager
下载图片的方法只有一个:
[SDWebImageManager.sharedManager loadImageWithURL:options:progress:completed:]
这个方法就是SDImageCache类在工作:
SDImageCache
// ============== SDImageCache.m ============== //
@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唯一子线程;
下面具体看一下这个方法具体干了什么:
- 对url进行处理,防止传进来的是NSString或NSNull
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
// ,很常见的错误是使用NSString对象而不是NSURL发送URL。由于一些奇怪的原因,Xcode不会对这种类型不匹配抛出任何警告。这里我们通过允许url作为NSString传递来避免这个错误。
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止应用程序在参数类型错误时崩溃,比如发送NSNull而不是NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
- 创建SDWebImageCombinedOperation
继承自
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
SDWebImageCombinedOperation
这个类比较简单,只保存了下面这些属性:
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@property (weak, nonatomic, nullable) SDWebImageManager *manager;
downloadToken和cacheOperation会在下面的流程中创建,用弱引用保存了SDWebImageManager是为了执行取消操作:[self.manager safelyRemoveOperationFromRunning:self];
- 处理failedUrl
failedURLs
是NSMutableSet<NSURL *>
,里面保存了失败过的URL。如果url的地址为空,或者该URL请求失败过且没有设置重试SDWebImageRetryFailed
选项,则直接直接调用完成。
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
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;
}
// failedURLsLock是dispatch_semaphore_t,用来保证读写failedURLs的线程安全。
// 接收一个信号和时间值,若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者经过输入的时间值;若信号量大于0,则会使信号量减1并返回,程序继续住下执行
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// 使信号量加1并返回
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
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;
}
- 保存
SDWebImageCombinedOperation
,根据url生成cacheKey
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];
// 我们可以设置@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;来自定义cacheKey。
- 解析缓存策略参数
SDImageCacheQueryDataWhenInMemory:默认当在内存找到结果后就不再到磁盘缓存查找,设置这个属性后强制到磁盘缓存查找。
SDImageCacheQueryDiskSync:默认情况下内存缓存同步,磁盘缓存异步,这个属性可以强制磁盘缓存同步。
SDImageCacheScaleDownLargeImages:默认不会对图片进行缩放,这个属性会根据设备的内存对图片进行适当的缩放。
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
......
从缓存加载
到SDImageCache
创建cacheOperation
。
// 弱引用避免保留环
__weak SDWebImageCombinedOperation *weakOperation = operation;
//在SDImageCache里查询是否存在缓存的图片
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
这里看一下是怎么查询的。- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock
:
首先判断key存在吗
// 这里的key就是url返回的本地标识
if (!key) {
// 如果不存在回调block参数为空
// typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
SDMemoryCache
就是NSCache
,当收到内存警告时会清除缓存。
- NSCache
NSCache是一个可变的集合类型,用于临时存放键值对,当资源不足时会被移除。
另外还有两个关键属性:
var countLimit: Int
能最大存放对象的数量
var totalCostLimit: Int
在开始释放对象之前,cache最多能持有cost的数量
需要注意的是totalCostLimit中的cost是根据setObject(ObjectType, forKey: KeyType, cost: Int)函数中的cost参数进行计算,也就是说cost需要使用者计算后存入。
如果没有设置SDImageCacheQueryDataWhenInMemory
属性,那找到图片后直接返回:
// 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
,也就是返回的cacheOperation
它只是用来操控是否取消磁盘读取:
//================查看磁盘的缓存=================//
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];
// self.config.shouldCacheImagesInMemory:是否使用内存缓存
if (diskImage && self.config.shouldCacheImagesInMemory) {
// cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
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);
});
}
}
}
};
最后根据SDImageCacheQueryDiskSync来决定是异步执行还是同步执行。
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
从缓存中查询看看接下来的操作
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 如果执行过程中操作取消,安全移除操作
// return 是跳出这个block,直接结束
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 1. 如果不存在缓存图片,或者需要刷新缓存 2. 代理可以响应方法,或者代理直接执行该方法,即从网络下载图片3.不能是可以防止从网络下载映像,只从内存读取这个标识。
// 1 和 2 和3 是并且关系
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
满足上面的三点就可以从网络下载。
先介绍一下SDWebImageDownloader。
SDWebImageDownloader
属性:
// ============== SDWebImageDownloader.m ============== //
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;//下载队列
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;//最后添加的下载操作
@property (assign, nonatomic, nullable) Class operationClass;//操作类
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;//操作数组
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;//HTTP请求头
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;//用来阻塞前面的下载线程(串行化)
核心方法就是下载图片:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
从网络加载
首先确认了上面的三个条件,就算开始网络加载了,不过在调用上面的方法前,需要获取下载配置项downloaderOptions。
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
// 如果在缓存中找到了图片,但是设置了SDWebImageRefreshCached,因此要NSURLCache重新从服务器下载
// 先调用completeBlock后续进行网络下载
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
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) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
SDWebImageDownloaderLowPriority:低优先级
SDWebImageDownloaderProgressiveDownload:边加载变显示
SDWebImageDownloaderUseNSURLCache:使用NSURLCache
SDWebImageDownloaderContinueInBackground:App进入后台后继续下载图片
SDWebImageDownloaderHandleCookies:处理NSHTTPCookieStore里面的Cookies
SDWebImageDownloaderAllowInvalidSSLCertificates:忽略SSL证书
SDWebImageDownloaderHighPriority:高优先级
SDWebImageDownloaderScaleDownLargeImages:缩放图片
如果图片已缓存,只是刷新缓存,则强制取消SDWebImageDownloaderProgressiveDownload 且忽略NSURLCache的缓存SDWebImageDownloaderIgnoreCachedResponse。
- 开始调用SDWebImageDownloader下载:
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
// 下载完成后的操作
}];
那么这个方法具体做了什么呢?点进去看一下
a, 首先判断url是否为空:
// URL将被用作回调字典的键,所以它不能为nil。如果为nil,立即调用没有图像或数据的已完成块。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
b, 获取downloadOperation
LOCK(self.operationsLock);
id downloadOperationCancelToken;
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
c, 删除已经完成或被取消或者不存在的操作
// 有一种情况是,操作可能被标记为完成或取消,但没有从' self. uroperations '中删除。
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
d, 设置progressBlock、completedBlock
// 将url和操作标识存入操作字典中
[self.URLOperations setObject:operation forKey:url];
// if 操作
// Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
//根据Apple文档完成所有配置后,才添加操作到操作队列。// ' addOperation: '不会同步执行' operation.completionBlock ',所以不会导致死锁。
[self.downloadQueue addOperation:operation];
// else 操作
//当我们重用下载操作附加更多的回调,可能存在线程安全问题,因为getter的回调可能在另一个队列(解码队列或委托队列 // 我们这里锁操作,在“SDWebImageDownloaderOperation”,我们使用“@synchonzied(self),以确保这两个类之间的线程安全。
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
e, 生成SDWebImageDownloadToken
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;