SDWebImage源码解析(一)
2019-03-05 本文已影响0人
lucky雄
1、概述
类关系图.pngSDWebImage基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清理等一些列功能。让我们专心于业务的处理。但是并不意味着会用就可以了,通过源码分析和学习,让我们知道如何用好它。学习分析优秀源码也可以从潜移默化中给我们提供很多解决日常需求的思路。下面就是一张图来概述SDWebImage的所有类:
通过对这个图片的分析,我们可以把SDWebImage的源码分为三种:
- 各种分类:
-
UIButton(WebCache)
为UIButton
类添加载图片的方法。比如正常情况下、点击情况下、的image属性和背景图片等。 -
MKAnnotationView(WebCache)
为MKAnnotationView
类添加各种加载图片的方法。 -
UIImageView(WebCache)
为UIImageView
类添加加载图片的方法。 -
UIImageView(HighlightedWebCache)为
UIImageView`类添加高亮状态下加载图片的方法。 -
FLAnimatedImageView(WebCache)
为FLAnimatedImageView
类添加加载动态的方法,这个分类需要引入FLAnimatedImage框架。SDWebImage推荐使用这个框架来处理动态图片(GIF)的加载。 -
UIImageView
、UIButton
、FLAnimatedImageView
通过sd_setImageWithURL
等api来做图片加载请求。这也是我们唯一需要做的。 - 上面的几个
UIView子
类都会调用UIView(WebCache)
分类的sd_internalSetImageWithURL
方法来做图片加载请求。具体是通过SDWebImageManager
调用来实现的。同时实现了 Operation取消、ActivityIndicator的添加与取消。
-
- 各种工具类:
-
NSData+ImageContentType
: 根据图片数据获取图片的类型,比如GIF、PNG等。 -
SDWebImageCompat
: 根据屏幕的分辨倍数成倍放大或者缩小图片大小。 -
SDImageCacheConfig
: 图片缓存策略记录。比如是否解压缩、是否允许iCloud、是否允许内存缓存、缓存时间等。默认的缓存时间是一周。 -
UIImage+MultiFormat
: 获取UIImage对象对应的data、或者根据data生成指定格式的UIImage,其实就是UIImage和NSData之间的转换处理。 -
UIImage+GIF
: 对于一张图片是否GIF做判断。可以根据NSData返回一张GIF的UIImage对象,并且只返回GIF的第一张图片生成的GIF。如果要显示多张GIF,使用FLAnimatedImageView。 -
SDWebImageDecoder
: 根据图片的情况,做图片的解压缩处理。并且根据图片的情况决定如何处理解压缩。
-
- 核心类:
-
SDImageCache
负责SDWebImage的整个缓存工作,是一个单列对象。缓存路径处理、缓存名字处理、管理内存缓存和磁盘缓存的创建和删除、根据指定key获取图片、存入图片的类型处理、根据缓存的创建和修改日期删除缓存。 -
SDWebImageManager
: 拥有一个SDWebImageCache和SDWebImageDownloader属性分别用于图片的缓存和加载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合。这个类是一个单列。还有就是各种加载选项的处理。 -
SDWebImageDownloader
: 实现了图片加载的具体处理,如果图片在缓存存在则从缓存区。如果缓存不存在,则直接创建一个。 SDWebImageDownloaderOperation对象来下载图片。管理NSURLRequest对象请求头的封装、缓存、cookie的设置。加载选项的处理等功能。管理Operation之间的依赖关系。这个类是一个单列. -
SDWebImageDownloaderOperation
: 一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。 -
UIView+WebCache
: 所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有 UIView及其子类的分类都是用这个类的sd_intemalSetImageWithURL:来实现图片的加载。 -
FLAnimatedImageView
: 动态图片的数据通过ALAnimatedImage对象来封装。-FLAnimatedImageView是UIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView做了流程优化。
-
2、实现流程
流程图.png
SDWebImage
为我们实现了图片加载、数据处理、图片缓存等一些列工作。通过下图我们可以分析一下他的流程:
通过这个图,我们发现SDWebImage加载的过程是首先从缓存中加载数据。而且缓存加载又是优先从内存缓存中加载,然后才是磁盘加载。最后如果缓存没有,才从网络上加载。同时网络成功加载图片以后,存入本地缓存。
3、UIView+WebCache 、UIView+WebCacheOperation分析
UIImageView
、UIButton
、FLAnimatedImageView
都会调用UIView(WebCache)
分类的sd_internalSetImageWithURL
方法来做图片加载请求。具体是通过SDWebImageManager
调用来实现的。同时实现了Operation取消、ActivityIndicator的添加与取消。我们首先来看sd_internalSetImageWithURL
方法的实现:
/**
《核心方法》
使用image的url和占位符设置imageView的image
@param url 图像的URL
@param placeholder 初始化的占位图,直到url请求完成
@param options 下载图片时的选项,详见 SDWebImageOptions
@param operationKey 操作的关键字,如果为空,则为类的名字
@param setImageBlock 设置图片时的回调代码
@param progressBlock 图片下载时的回调,注意执行是在后台的队列
@param completedBlock 所有操作完成时回调,不返回值
*并将请求的uiimage作为第一个参数。如果出现错误,图像参数
*为零,第二个参数可能包含nserrror。第三个参数是布尔值
*指示是从本地缓存还是从网络检索图像。
*第四个参数是原始图像URL。
@param context 具有执行指定更改或进程的额外信息的上下文。
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context {
/**
1、停止所有和这个View相关的Operation。
这里还有一个operation 字典:UIView+WebCacheOperation 有一个关联对象,用来保存这个View相关的Operation
@param operationKey 操作的关键字,如果为空,则为类的名字
UIView+WebCacheOperation 分类用来保存view相关的操作
*/
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
/** 存url
* 把UIImageView的加载图片操作和他自身用关联对象关联起来,方便后面取消等操作。关联的key就是UIImageView对应的类名
*/
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/** 如果有设置SDWebImageDelayPlaceholder,则先显示站位图 */
dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];
if (!(options & SDWebImageDelayPlaceholder)) {
if (group) {
dispatch_group_enter(group);
}
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
/** 如果UIImageView对象有设置添加转动菊花数据,加载的时候添加转动的菊花 */
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
#endif
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
// 定义完成进度的block回调
__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);
}
};
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;
if (!sself) { return; }
#if SD_UIKIT
//停止菊花
[sself sd_removeActivityIndicator];
#endif
// if the progress not been updated, mark it to complete state
/** 如果progress没有更新,设置总量和完成量为未知状态 */
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
/** 如果设置了不自动显示图片,则直接调用completedBlock,让调用者处理图片的显示
*
* finished or SDWebImageAvoidAutoSetImage --->应该 调用CompletedBlock
*/
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
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
/**
* 得到image and (SDWebImageAvoidAutoSetImage在completeblock 中设置图片)---> 不自动显示
* 没得到image and (默认显示了SDWebImageDelayPlaceholder占位图) ----> 不自动显示。
* 不自动显示 ---> 代用completedBlock
*/
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
/** ⏬ 设置imageview的image */
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
/**
* 得到image and 没有设置SDWebImageAvoidAutoSetImage
*/
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
/**
* 没得到image and 没有设置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);
}
});
}
}
还有就是通过
UIView+WebCacheOperation
类来实现UIView的图片下载Operation的关联和取消。具体key的值可以从sd_internalSetImageWithURL
中找到具体获取方式,通过在这个方法中实现Operation的关联与取消。
/**
关联Operation对象与key对象
@param operation Operation对象
@param key key
*/
- (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];
}
}
}
}
/**
取消当前key对应的所有实现了SDWebImageOperation协议的Operation对象
@param key Operation对应的key
*/
- (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];
}
}
}
}