SDWebImage是iOS开发中被广泛使用的第三方开源库,它提供了图片从加载、解析、缓存、清理等一系列功能。
平时我们引进这个第三方库,最主要的就是使用sd_setImageWithURL
方法。下面讲讲这个方法的调用
通过上图,可以知道SDWebImage加载的过程是首先从缓存中加载数据,缓存加载是优先从内存缓存中加载,然后才是磁盘加载。如果没有缓存,才从网络上加载。网络成功加载图片以后,存入本地缓存。
根据自己需要选择UIImageView+WebCache类中以下任一方法:
这个类主要是实现以下方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context;
- (void)sd_setImageWithURL:(nullable NSURL *)url
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
其实以上所有的方法都是在调用下面这个方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
而这个方法则是调用了UIView+WebCache类中的方法。
上面UIImageView+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;
在这个方法里,创建了一个SDWebImageManager
对象,然后去调用其类的loadImageWithURL:()
。
所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过
UIView+WebCacheOperation
分类来管理请求的取消和记录工作。所有UIView及其子类的分类都是用这个类的来实现图片的加载,这个类也是主要实现了这个方法。
上面的方法会调用SDWebImageManager
类的:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;
在 SDWebImageManager
对象的上述方法里,首先会查询在缓存中有没有这个图片,然后根据各种 option 判断决定是否要从网络端下载。而查询缓存中有没有是通过调用 SDImageCache
对象的实例方法来实现的。
上面的方法会调用SDImageCache
类的下面方法从图像缓存中查询给定密钥的缓存图像,如果映像缓存在内存中,则同步调用完成;否则异步调用完成。
- (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
SDImageCache
这个类是专门负责缓存相关的问题的,包括查询缓存和将图片进行缓存。SDImageCache
使用了一个 NSCache
对象来进行内存缓存,磁盘缓存则是把图片数据存放在应用沙盒的Caches这个文件夹下。
这里的查询缓存的步骤为:首先查询内存缓存,内存缓存查询完了以后再判断是否需要查询磁盘缓存。如果查询内存缓存已经有了结果并且没有设置一定要查询磁盘缓存,那么就不查询磁盘缓存,否则就要查询磁盘缓存。内存缓存没有查询到图片,并且磁盘缓存查询到了图片,那么就要把这个内容缓存到内存缓存中,返回缓存查询的结果;如果磁盘缓冲也没有查询到图片,就调用下一个类去下载图片。
下载未缓冲过的图片时,使用类SDWebImageDownloader
下面这个方法:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
这个分类在上面介绍调用关系时已经说过了,里面的方法都是在为程序员的不同需求提供对外的接口方法,最后都是调用
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
这个方法,没什么说的。
这个类分析一下- (void)sd_internalSetImageWithURL:()方法:
- (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 {
if (context) {
// copy to avoid mutable object
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
//下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;
// 获取图像管理者对象
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
// 删除此管理器以避免保留周期(管理器 -> 加载程序 -> 操作 -> 上下文 -> 管理器)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
BOOL shouldUseWeakCache = NO;
if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
}
if (!(options & SDWebImageDelayPlaceholder)) {
if (shouldUseWeakCache) {
NSString *key = [manager cacheKeyForURL:url context:context];
// call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
// this unfortunately will cause twice memory cache query, but it's fast enough
// in the future the weak cache feature may be re-design or removed
[((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
}
dispatch_main_async_safe(^{
//设置下载图片完成之前的占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
// 判断url是否有效
if (url) {
// reset the progress
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
// 获取图像加载进度
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// check and start image indicator
// 是否显示进度条
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
@weakify(self);
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
@strongify(self);
if (!self) { return; }
// if the progress not been updated, mark it to complete state
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// check and stop image indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
if (!self) { return; }
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, 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
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClosure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
BOOL shouldUseTransition = NO;
if (options & SDWebImageForceTransition) {
// Always
shouldUseTransition = YES;
} else if (cacheType == SDImageCacheTypeNone) {
// From network
shouldUseTransition = YES;
} else {
// From disk (and, user don't use sync query)
if (cacheType == SDImageCacheTypeMemory) {
shouldUseTransition = NO;
} else if (cacheType == SDImageCacheTypeDisk) {
if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
shouldUseTransition = NO;
} else {
shouldUseTransition = YES;
}
} else {
// Not valid cache type, fallback
shouldUseTransition = NO;
}
}
if (finished && shouldUseTransition) {
transition = self.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
callCompletedBlockClosure();
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
dispatch_main_async_safe(^{
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}
一行一行分析一下:
首先是方法名:
其总共有五个参数,URL就是我们需要下载的在线图片链接,placeholder(占位符)Image其是UIImage类型,而SDWebImageOptions我们查看其源码并进行相关信息的查询
其是一种暴露在外的可供使用者使用的选择方法
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
*默认情况下,当URL下载失败时,该URL会被列入黑名单,因此库不会继续尝试。
*此标志禁用此黑名单。
*/
SDWebImageRetryFailed = 1 << 0,
/**
*默认情况下,图像下载是在UI交互期间启动的,此标志禁用此功能,
*例如,导致在UIScrollView上延迟下载。
*/
SDWebImageLowPriority = 1 << 1,
/**
*此标志启用渐进式下载,图像在下载过程中会像浏览器一样渐进式显示。
*默认情况下,图像仅在完全下载后显示。
*/
SDWebImageProgressiveLoad = 1 << 2,
/**
*即使映像已缓存,也要遵守HTTP响应缓存控制,并在需要时从远程位置刷新映像。
*磁盘缓存将由NSURLCache而不是SDWebImage处理,这会导致性能略有下降。
*此选项有助于处理同一请求URL后面更改的图像,例如Facebook graph api配置文件图片。
*如果刷新了缓存的图像,则使用缓存的图像调用一次完成块,然后使用最终图像调用一次完成块。
*
*只有当不能使用嵌入式缓存破坏参数使URL保持静态时,才使用此标志。
*/
SDWebImageRefreshCached = 1 << 3,
/**
* In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
* extra time in background to let the request finish. If the background task expires the operation will be cancelled.
*/
SDWebImageContinueInBackground = 1 << 4,
/**
*通过设置
*NSMutableURLRequest。HTTPShouldHandleCookies=是;
*/
SDWebImageHandleCookies = 1 << 5,
/**
*启用以允许不受信任的SSL证书。
*用于测试目的。在生产中小心使用。
*/
SDWebImageAllowInvalidSSLCertificates = 1 << 6,
/**
*默认情况下,图像按其排队顺序加载。此标志将它们移动到
*排在队伍前面。
*/
SDWebImageHighPriority = 1 << 7,
/**
*默认情况下,在加载图像时加载占位符图像。此标志将延迟加载
*直到图像加载完毕。
*/
SDWebImageDelayPlaceholder = 1 << 8,
/**
*我们通常不会对动画图像应用变换,因为大多数变形金刚无法管理动画图像。
*无论如何都要使用此标志来变换它们。
*/
SDWebImageTransformAnimatedImage = 1 << 9,
/**
*默认情况下,图像在下载后添加到imageView。但在某些情况下,我们希望
*在设置图像之前先用手(例如,应用过滤器或添加交叉淡入淡出动画)
*如果要在成功时手动设置完成中的图像,请使用此标志
*/
SDWebImageAvoidAutoSetImage = 1 << 10,
/**
*默认情况下,图像将根据其原始大小进行解码。
*此标志将图像缩小到与设备受限内存兼容的大小。
*要控制内存字节的限制,请选中“SDImageCoderHelper”。defaultScaleDownLimitBytes`(在iOS上默认为60MB)
*这实际上将转换为使用上下文选项“”。imageThumbnailPixelSize`来自v5。5.0(在iOS上默认为(39663966)。以前没有。
*以及v5和动画效果中的标志。5.0. 以前没有。
*@note如果你需要细节控件,最好使用上下文选项“imageThumbnailPixelSize”和“ImagePreserveApectRatio”。
*/
SDWebImageScaleDownLargeImages = 1 << 11,
/**
*默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。这个掩码可以强制同时查询图像数据。但是,除非指定'SDWebImageQueryMemoryDataSync',否则此查询是异步的`
*/
SDWebImageQueryMemoryData = 1 << 12,
/**
*默认情况下,当您只指定'SDWebImageQueryMemoryData'时,我们会异步查询内存图像数据。并结合该掩码同步查询内存图像数据。
*@note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元重用期间闪烁。
*/
SDWebImageQueryMemoryDataSync = 1 << 13,
/**
*默认情况下,当内存缓存丢失时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
*@note这3个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参见wiki页面。
*@note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元重用期间闪烁。
*/
SDWebImageQueryDiskDataSync = 1 << 14,
/**
*默认情况下,当缓存丢失时,将从加载程序加载图像。此标志可防止仅从缓存加载。
*/
SDWebImageFromCacheOnly = 1 << 15,
/**
*默认情况下,我们在从加载程序加载图像之前查询缓存。此标志可防止仅从加载程序加载。
*/
SDWebImageFromLoaderOnly = 1 << 16,
/**
*默认情况下,当您在映像加载完成后使用“SDWebImageTransition”执行某些视图转换时,此转换仅在manager的回调异步(从网络或磁盘缓存查询)时应用于映像
*此掩码可以强制对任何情况应用视图转换,如内存缓存查询或同步磁盘缓存查询。
*/
SDWebImageForceTransition = 1 << 17,
/**
*默认情况下,我们将在缓存查询和从网络下载期间在后台解码图像。这有助于提高性能,因为在屏幕上渲染图像时,首先需要对其进行解码。但这种情况是通过核心动画在主队列中发生的。
*然而,这个过程也可能会增加内存使用。如果由于内存消耗过多而遇到问题,此标志可能会阻止对图像进行解码。
*/
SDWebImageAvoidDecodeImage = 1 << 18,
/**
*默认情况下,我们解码动画图像。此标志只能强制解码第一帧并生成静态图像。
*/
SDWebImageDecodeFirstFrameOnly = 1 << 19,
/**
*默认情况下,对于“SDAnimatedImage”,我们在渲染期间解码动画图像帧以减少内存使用。但是,当动画图像被许多ImageView共享时,可以指定将所有帧预加载到内存中,以减少CPU使用。
*这实际上会在后台队列中触发“Preload AllanimatedImageFrames”(仅限磁盘缓存和下载)。
*/
SDWebImagePreloadAllFrames = 1 << 20,
/**
*默认情况下,当您使用“SDWebImageContextAnimatedImageClass”上下文选项时(比如使用“SDAnimatedImageView”,其设计目的是使用“SDAnimatedImageView”),当内存缓存命中时,或者图像解码器无法生成与自定义类完全匹配的图像作为备用解决方案时,我们仍然可以使用“UIImage”。
*使用此选项,可以确保我们始终使用您提供的类回调图像。如果生成失败,将使用代码为“SDWebImageErrorBadImageData”的错误。
*注意:此选项与“SDWebImageDecodeFirstFrameOnly”不兼容,后者总是生成UIImage/NSImage。
*/
SDWebImageMatchAnimatedImageClass = 1 << 21,
/**
*默认情况下,当我们从网络加载映像时,映像将被写入缓存(内存和磁盘,由“storeCacheType”上下文选项控制)
*这可能是一个异步操作,最终的'SDInternalCompletionBlock'回调不能保证磁盘缓存写入完成,可能会导致逻辑错误。(例如,您仅在完成块中修改磁盘数据,但磁盘缓存尚未就绪)
*如果需要使用完成块中的磁盘缓存进行处理,则应使用此选项确保在回调时已写入磁盘缓存。
*注意:如果在使用自定义缓存序列化程序或transformer时使用此选项,我们还将等待写入的输出图像数据完成。
*/
SDWebImageWaitStoreCache = 1 << 22,
/**
*我们通常不会对矢量图像应用变换,因为矢量图像支持动态更改为任何大小,光栅化为固定大小会丢失细节。要修改矢量图像,可以在运行时处理矢量数据(例如修改PDF标记/SVG元素)。
*无论如何都要使用此标志来变换它们。
*/
SDWebImageTransformVectorImage = 1 << 23
};
//从context获取对应的validOperationKey,然后根据参数validOperationKey进行后面的操作,
//如果operationKey为nil key取NSStringFromClass([self class])
if (context) {
// copy to avoid mutable object
//避免不可变对象
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
取消之前的下载任务。通过调用UIView+WebCacheOperation类中的方法,
在UIView+WebCacheOperation.h的API都是对下载operation的操作。绑定operation、取消operation、移除operation。在UIView+WebCacheOperation.m同样使用关联对象针对每个UIKit对象在内存中维护一个字典operationDictionary。可以对不同的key值添加对应的下载operation,也可以在下载操作没有完成的时候根据key取到operation进行取消。
以下是该方法的实现:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// 从队列中取消正在进行的下载程序
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
operationDictionary的key一般是类名,如此同一个UIImageView同时调用两次,第一次的下载操作会先被取消,然后将operationDictionary的中的operation对应到第二次的下载操作。
然后了解一下是如何将之前的下载任务从队列中取消:
先为UIView+WebCache 创建operationDictionary。
调用该类中的sd_operationDictionary方法:
- (SDOperationsDictionary *)sd_operationDictionary {
@synchronized(self) {
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
通过键值来得到绑定的的关联对象的值赋值给可变字典类型的operations,如果operations不为空,那么返回该关联对象的值。
然后:
如果有两个键值相同的内容进入队列,那么通过前面字典的创建得到的关联对象的值也是一样的,传入key在返回的字典中查找是否已经存在,如果存在则取消所有操作,conformsToProtocol
方法如果符合这个协议(协议中声明了取消方法),也调用协议中的取消方法。
实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。
取消队列中的下载任务的好处:
1… 因为其是针对的一个UIImageView,取消前一个操作,省时、省流量。
避免SDWebImage的复用。 也就是避免对一张图片进行重复下载。加载图片完成后, 回调时会2. 先检查任务的Operation还在不在, 不在,则不回调显示, 反之回调显示并移除Operation。
3. 当程序中断导致链接失效时,当前的下载还在操作队列中,但是按道理应该是失效状态,我们可以通过先取消当前正在进行的下载来保证操作队列中的链接不存在这种情况。
创建图像管理器,如果没有创建过的话,就使用单例创建;如果创建过,就删除此管理器。
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
//删除此管理器以避免保留周期(管理器->加载程序->操作->上下文->管理器)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
if (shouldUseWeakCache) {
NSString *key = [manager cacheKeyForURL:url context:context];
//调用内存缓存以触发弱缓存同步逻辑,忽略返回值并继续正常查询
//不幸的是,这将导致两次内存缓存查询,但速度足够快
//将来可能会重新设计或删除弱缓存功能
[((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
}
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
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
在主线程调用了
判断传入的URL是否为空,如果不为空,则获取图像加载进度,然后置0。
if (url) {
// reset the progress
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
// 获取图像加载进度
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
加载小菊花等图像指示器。
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
先看一下SDWebImageIndicator定义:
这是调用这个方法
- (void)sd_startImageIndicator {
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
if (!imageIndicator) {
return;
}
dispatch_main_async_safe(^{
[imageIndicator startAnimatingIndicator];
});
}
然后在主线程调用这个方法:
- (void)startAnimatingIndicator {
#if SD_UIKIT
[self.indicatorView startAnimating];
#else
[self.indicatorView startAnimation:nil];
#endif
self.indicatorView.hidden = NO;
}
下面主要是调用这个方法:
- (nullable SDWebImageCombinedOperation *)
loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;
获取一个加载任务。下面是它的具体实现:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 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.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(_failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(_failedURLsLock);
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
return operation;
}
//把加载图片的一个载体存入runningOperations。里面是所有正在做图片加载过程的operation的集合。
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);
// Preprocess the options and context arg to decide the final the result for manager
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// Start the entry to load image from cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
先是检查URL的合法性,然后
经过上面的url验证之后会调用callCacheProcessForOperation
方法查找缓存,
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Grab the image cache to use
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// Get the query cache type
SDImageCacheType queryCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
}
// Check whether we should query cache
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType 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;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
// Have a chance to query original cache instead of downloading
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
精简一下就是:
BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
if (shouldQueryCache) {
// 缓存查找
}else {
// 进行下载操作
}
缓冲查找的方法是:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// Invalid cache type
if (queryCacheType == SDImageCacheTypeNone) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}
if (image) {
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
Class animatedImageClass = image.class;
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// Second check the disk cache...
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
NSData* (^queryDiskDataBlock)(void) = ^NSData* {
if (operation.isCancelled) {
return nil;
}
return [self diskImageDataBySearchingAllPathsForKey:key];
};
UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
if (operation.isCancelled) {
return nil;
}
UIImage *diskImage;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
} else if (diskData) {
BOOL shouldCacheToMomery = YES;
if (context[SDWebImageContextStoreCacheType]) {
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
return diskImage;
};
// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
__block NSData* diskData;
__block UIImage* diskImage;
dispatch_sync(self.ioQueue, ^{
diskData = queryDiskDataBlock();
diskImage = queryDiskImageBlock(diskData);
});
if (doneBlock) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}
} else {
dispatch_async(self.ioQueue, ^{
NSData* diskData = queryDiskDataBlock();
UIImage* diskImage = queryDiskImageBlock(diskData);
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
});
}
return operation;
}
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
这里直接通过key去cache里面查找,如果这里找到了,并且没有强制指明查询磁盘的话直接回调
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
[self.memoryCache setObject:diskImage forKey:key cost:cost];
如果都没有找到,然后是下载图片:
在这个方法
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock
的回调block中,首先判断是否需要下载:
@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;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
// Have a chance to query original cache instead of downloading
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
如果需要下载,则调用SDWebImageDownloader的callDownloadProcessForOperation
。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
if (!operation) {
operation = createCallback();
__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);
};
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
/*------------这里添加下载任务到downQueue----------------*/
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
在这里把operation添加到下载队列self.downloadQueue中,self.downloadQueue是NSOperationQueue的实例,它默认的最大并发数为6(这个值可以修改)。
然后就使用NSURLSession下载,所以在SDWebImageDownloader有着NSURLSession的代理方法。
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
[dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(proposedResponse);
}
}
}
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
[dataOperation URLSession:session task:task didCompleteWithError:error];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
[dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(request);
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
[dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) {
[dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics];
}
}
这些方法都调用了(NSOperation *)operationWithTask:(NSURLSessionTask *)task。
通过task获取对应的SDWebImageDownloaderOperation实例,将具体的操作放到了这个类里面,在这个类里面这里面对需要解码的图片做了解码操作和判断是否图片需要缩放等处理。
经过层层回调,结果会回到SDWebImageManager的- (id
方法中- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
的completedBlock中,这里调用了[self storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
方法做图片缓存操作,我们来看一下实现细节。
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
if (!toDisk) {
if (completionBlock) {
completionBlock();
}
return;
}
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// If image is custom animated image class, prefer its original animated data
data = [((id<SDAnimatedImage>)image) animatedImageData];
}
if (!data && image) {
// Check image's associated image format, may return .undefined
SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined) {
// If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
if (image.sd_isAnimated) {
format = SDImageFormatGIF;
} else {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
}
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
[self _archivedDataWithImage:image forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
可以看到self.memCache直接存入内存
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
如果需要存入磁盘的话,在异步队列self.ioQueue中调用方法写入磁盘,
所有的缓存都有时效和大小的限制,在SDImageCache中有着一些列的清理策略 默认图片的有效缓存期为7天:
[self.memCache removeAllObjects];
这个方法是下载图片操作,
// 下载图片操作
// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
看看它是怎么实现的:
- (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];
}
}
}
}
所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。
这个方法涉及的方法很多,简单说一下流程:
SDWebImageContext
复制并转换为 mutable
,获取其中的 validOperationKey
值作为校验 id,默认值为当前 view
的类名;sd_cancelImageLoadOperationWithKey
取消上一次任务,保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突;SDWebImageManager 、SDImageLoaderProgressBlock
, 重置 NSProgress、SDWebImageIndicator
;loadImageWithURL:
并将返回的 SDWebImageOperation
存入 sd_operationDictionary
,key
为 validOperationKey
;indicator
。SDWebImage 的图片缓存采用的是 Memory(内存) 和 Disk(硬盘) 双重 Cache 机制。该功能是由SDImageCache 类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对 UI 操作造成影响。
内存缓冲主要是利用NSCache对象来实现的。NSCache是一个类似于集合的容器,提供了类似可变字典存储 key-value
对的实现方式,但它比可变字典更适用于实现缓存。
- 最重要的原因是NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存的时候需要考虑加锁和释放锁,NSCache已经帮我们做好了这一步。
- 其次,内存不足时NSCache会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步删除对象的操作。
- 还有一点NSCache的键key不会被复制,所以key不需要实现NSCopying协议。
使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache 还定义了一个串行队列,来异步存储图片。
SDImageCache 提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key 值来索引它。 在内存中,我们将其作为 NSCache 的 key 值,而在磁盘中,我们用这个 key 作为图片的文件名。 对于一个远程服务器下载的图片,其 url 是作为这个 key 的最佳选择了。
Disk(硬盘)缓存清理策略:SDWebImage 会在每次 APP 结束的时候执行清理任务。 清理缓存的规则分两步进行。 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够。 那么就继续按文件时间从早到晚排序,先清除最早的缓存文件, 直到剩余空间达到要求。
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52: {
if (data.length >= 12) {
//RIFF....WEBP
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
break;
}
case 0x00: {
if (data.length >= 12) {
//....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
if ([testString isEqualToString:@"ftypheic"]
|| [testString isEqualToString:@"ftypheix"]
|| [testString isEqualToString:@"ftyphevc"]
|| [testString isEqualToString:@"ftyphevx"]) {
return SDImageFormatHEIC;
}
//....ftypmif1 ....ftypmsf1
if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
return SDImageFormatHEIF;
}
}
break;
}
case 0x25: {
if (data.length >= 4) {
//%PDF
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding];
if ([testString isEqualToString:@"PDF"]) {
return SDImageFormatPDF;
}
}
}
case 0x3C: {
// Check end with SVG tag
if ([data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound) {
return SDImageFormatSVG;
}
}
}
return SDImageFormatUndefined;
}
直接截取前一个字节长度的数据:
uint8_t c;
[data getBytes:&c length:1];
以后遇到再补充吧