该文章阅读的SDWebImage的版本为4.3.3。
1.日常使用
在日常的使用中,通常是加载网络图片到UIImageView上展示,所以一般在需要使用SDWebImage的文件中只引用#import "UIImageView+WebCache.h"头文件。
最简单的加载方式是只加载图片地址:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:imageView];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]];
当然,SDWebImage也提供了其他的加载方法,不过点击方法进入查看后,发现最终都是调用其全能方法:
- (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
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)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];
}
可以发现,全能方法并没有什么实际的实现,只是对另一个方法的封装。
2.再进一步
在上一段中我们进入了UIImageView+WebCache分类中查看图片加载的实现,发现其实是调用了另外一个方法,而这个方法在另一个分类UIView+WebCache中:
- (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;
接着看这个方法的实现:
- (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];
}
发现这个方法也是调用了一个全能方法:
- (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
context:(nullable NSDictionary<NSString *, id> *)context;
再进入到这个方法中查看,发现这个方法就是加载图片的核心实现
3.核心实现
通过不断的点击进入方法查看,我们从UIImageView+WebCache分类跳到UIView+WebCache分类,并找到了其核心实现:
- (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
context:(nullable NSDictionary<NSString *, id> *)context {
// 生成一个有效的操作密钥,如果传入了参数就用传入的,否则就用当前类的类名
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 取消该密钥对应的图片加载操作
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 通过关联对象,将传入的图片地址保存到静态变量imageURLKey中
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 如果没有选择延迟加载占位图
if (!(options & SDWebImageDelayPlaceholder)) {
// 如果context中有调度组,就获取并进入调度组
if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
dispatch_group_enter(group);
}
// 在主线程主队列中设置占位图
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
// 如果传入了图片链接
if (url) {
// 如果设置要显示加载小菊花,就添加加载小菊花并开始动画
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
// 初始化图片加载进度
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
// 生成图片管理者,如果context中有就用context中的,否则就直接生成
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
// 生成一个代码块用来在下载图片的方法中监听进度并进行回调
__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) {
// 如果没有生成强引用的self就终止执行
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
// 如果有加载小菊花的话就移除掉
[sself sd_removeActivityIndicator];
// 如果已经完成并且没有错误,并且进度没有更新,就将进度状态设为未知
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
// 是否应该回调完成blok: 如果已经完成或者设置了在设置图片前处理
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 是否应该不设置图片: 如果有图片但设置了在设置图片前处理,或者没有图片并且没有设置延迟加载占位图
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
// 生成完成回调代码块
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
// 如果没有生成强引用的self就终止执行
if (!sself) { return; }
// 如果需要设置图片就直接刷新视图
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
// 如果传入了回调block并且应该进行回调,就直接回调
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// 如果不需要设置图片就在主线程主队列中调用上面生成的完成回调代码块,并且不再向下执行
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
// 生成变量保存数据
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// 如果图片下载成功就用变量保存图片
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// 如果图片下载失败并且设置了延迟加载占位图,就保存占位图
targetImage = placeholder;
targetData = nil;
}
// 检查一下是否应该转换图片:如果下载完成,并且设置了图片强制转换或者图片缓存类型是不缓存直接从网络加载,就进行强制转换
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
// 如果context中有调度组,就获取并进入调度组
if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
dispatch_group_enter(group);
// 在主线程主队列中设置图片
dispatch_main_async_safe(^{
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
});
// 调度完成后调用完成回调代码块
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
callCompletedBlockClojure();
});
} else {
// 如果用户没有设置调度组,就直接在主线程主队列中设置图片和调用完成回调代码块
dispatch_main_async_safe(^{
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
callCompletedBlockClojure();
});
}
}];
// 根据密钥保存下载图片的操作
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
// 如果没传入图片链接,就在主线程主队列移除加载小菊花
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
// 如果传入了完成回调block就回调错误信息
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
这就是加载图片核心逻辑,接下来我们会看一下一些重要的方法
4.如何防止串图?
在使用SDWebImage的时候,大多数时候都是加载网络图片,为了保证UI的流畅性,通常是将加载过程放到子线程中。由于是异步加载,所以如果在同一个控件先后加载不同的图,比如使用复用机制的UITableViewCell,就可能出现串图的问题。SDWebImage是怎么解决的呢?我们来看一下。
在上面的方法实现中,我们可以在开始和结束的位置看到这样一对方法:
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
分别点击进入查看实现:
/**
这个方法是取消掉该控件上validOperationKey对应的图片加载操作
*/
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// 获取继承自NSMapTable的自定义字典对象
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];
}
}
}
/**
这个方法是将该控件上的图片加载操作和validOperationKey一一对应保存起来
*/
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
// 必须要传入参数key
if (key) {
// 先取消掉key对应的操作
[self sd_cancelImageLoadOperationWithKey:key];
// 必须要传入参数operation
if (operation) {
// 获取到保存用的字典,并在线程安全的状态下保存
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized (self) {
[operationDictionary setObject:operation forKey:key];
}
}
}
}
可以看到,在加载图片的一开始会先将该控件上validOperationKey对应的操作取消掉,然后在生成新的操作时将操作与validOperationKey一一对应保存起来,这样就能保证了在加载新图片时,一个控件上只有一个图片加载操作正在进行。
5.安全的主队列异步加载
在设置图片的时候,无论是加载好的图片还是占位图,都是在一个自定义的宏中进行的:
dispatch_main_async_safe(^{
// do something
});
我们点击这个宏进入查看:
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif
可以看到这个宏是对另一个宏dispatch_queue_async_safe(dispatch_get_main_queue(), block)的封装,其中一个参数传入了dispatch_get_main_queue()主队列,继续点击进入查看:
#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(queue)) == 0) {\
block();\
} else {\
dispatch_async(queue, block);\
}
- 其中
int strcmp(const char *__s1, const char *__s2);这个函数是用来比较两个字符串是否相等,如果返回0就说明两个传入的字符串相等。 - 还有
const char * dispatch_queue_get_label(dispatch_queue_t queue);这个函数是获取传入的队列的名字,如果传入了参数DISPATCH_CURRENT_QUEUE_LABEL,就是获取当前所在队列的名字。
所以这个宏的意思就是:如果当前所在队列是主队列就直接执行代码块;如果当前所在队列不是主队列就在主队列异步执行传入的代码块,其实就是确保传入的代码块block在主线程主队列中调用。
6.设置图片
无论是设置下载好的图还是占位图,都是调用了同一个方法:
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock {
[self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:0 imageURL:nil];
}
这个方法就是将图片设置到控件上的主方法
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
// 获取到当前视图
UIView *view = self;
// 设置临时变量保存设置图片代码块
SDSetImageBlock finalSetImageBlock;
// 如果设置了设置图片代码块就直接设置代码块
if (setImageBlock) {
finalSetImageBlock = setImageBlock;
}
#if SD_UIKIT || SD_MAC
// 如果是该类是图片视图,就创建设置图片代码块并在其中为图片视图设置图片
else if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = (UIImageView *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData) {
imageView.image = setImage;
};
}
#endif
#if SD_UIKIT
// 如果是该类是按钮,就创建设置图片代码块并在其中为按钮在正常状态下设置图片
else if ([view isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData){
[button setImage:setImage forState:UIControlStateNormal];
};
}
#endif
// 如果设置了过度
if (transition) {
#if SD_UIKIT
[UIView transitionWithView:view duration:0 options:0 animations:^{
// 0 duration to let UIKit render placeholder and prepares block
// 如果在展示过渡动画之前设置了要执行的代码块就先执行
if (transition.prepares) {
transition.prepares(view, image, imageData, cacheType, imageURL);
}
} completion:^(BOOL finished) {
// 开始执行动画
[UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
// 如果设置了代码块并且没设置避免自动设置图片,就直接传参并调用代码块
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
finalSetImageBlock(image, imageData);
}
// 如果设置了动画代码块,就传参并调用代码块
if (transition.animations) {
transition.animations(view, image);
}
// 如果设置了完成代码块,就传参并调用代码块
} completion:transition.completion];
}];
// 这是Mac相关
#elif SD_MAC
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) {
// 0 duration to let AppKit render placeholder and prepares block
prepareContext.duration = 0;
if (transition.prepares) {
transition.prepares(view, image, imageData, cacheType, imageURL);
}
} completionHandler:^{
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
context.duration = transition.duration;
context.timingFunction = transition.timingFunction;
context.allowsImplicitAnimation = (transition.animationOptions & SDWebImageAnimationOptionAllowsImplicitAnimation);
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
finalSetImageBlock(image, imageData);
}
if (transition.animations) {
transition.animations(view, image);
}
} completionHandler:^{
if (transition.completion) {
transition.completion(YES);
}
}];
}];
#endif
} else {
// 如果设置了设置图片代码块就传参并调用代码块
if (finalSetImageBlock) {
finalSetImageBlock(image, imageData);
}
}
}
总结一下设置图片的逻辑:
- 在方法的开始,根据控件所属类的不同,生成最终设置图片的代码块。
- 接下来如果设置了过度动画,就执行过度动画
- 如果没设置过度动画,就直接传参调用在方法开始生成的最终设置图片的代码块。
7.图片加载操作
接着我们来看最核心的方法,图片的加载:
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
这个方法除了必传的图片地址链接外,还提供了可选项、进度监听和完成回调。
点击进入查看方法实现,发现这个方法的实现有点多:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 完成回调代码块是必传的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 虽然参数要求传NSURL对象但是传NSStriing对象不会有警告,所以做一下处理
if ([url isKindOfClass:NSString.class]) {
// 如果传入的参数url是一个NSString类型对象,就转化成NSURL类型的
url = [NSURL URLWithString:(NSString *)url];
}
// 防止参数url的类型错误导致崩溃,如url的值是NSNull类型
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 生成一个图片加载操作的封装对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// 请求加载图片的地址连接是否在加载失败的地址链接集合中,也就是说这次请求加载的图片,以前是否加载失败过
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// 如果链接地址不正确,或者之前加载失败过但是也没设置失败可重试,就直接回调错误并返回
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];
}
// 生成地址链接对应的Key
NSString *key = [self cacheKeyForURL:url];
SDImageCacheOptions cacheOptions = 0;
// 如果设置了强制内存和硬盘同时查询
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
// 如果设置了强制内存和硬盘同步查询
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
// 生成查询图片缓存操作对象,并开始异步查询
__weak SDWebImageCombinedOperation *weakOperation = operation;
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;
}
// 查看是否应该从网络下载该图片,想要从网络下载图片必须同时满足以下条件:
// 没有设置只从缓存加载的选项
// 没找到缓存图,或者设置了需要刷新缓存图
// 代理对象没有实现这个代理方法,或者代理对象实现了这个代理方法并且代理方法返回了YES,意思是在缓存中找不到图片时要从网络上下载
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
// 如果需要下载图片
if (shouldDownload) {
// 如果有图片缓存并且设置了刷新图片缓存,就先进行完成回调
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.
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 获取设置的操作选项
SDWebImageDownloaderOptions downloaderOptions = 0;
// 低优先级
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
// 渐进显示
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
// 刷新缓存
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
// 后台下载
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
// 处理Cookies
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
// 允许不可信的SSL证书
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
// 高优先级
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
// 缩小尺寸
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
// 如果有缓存图,并且选择了刷新缓存的选项
if (cachedImage && options & SDWebImageRefreshCached) {
// 关闭渐进显示
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 忽视从NSURLCache中获取缓存
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 开启下载任务并获取下载任务的令牌
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// 如果没有操作对象,或者操作对象被取消了就什么也不做
} else if (error) {
// 如果出错,就进行回调
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
// 是否应该阻止下载已经失败的图片地址链接
BOOL shouldBlockFailedURL;
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
// 如果实现了代理就按照代理返回的数据
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
// 如果没有实现代理,想要阻止下载失败的链接就得满足以下条件:
不是没联网、
不是被取消、
不是连接超时、
不是关闭了国际漫游、
不是不允许蜂窝数据连接、
不是没有找到host、
不是无法连接host、
不是连接丢失
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
// 如果要组织失败连接再次下载,就把连接添加到黑名单中保存
if (shouldBlockFailedURL) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
// 如果下载成功
// 如果设置了尝试下载失败的链接选项,就把连接从黑名单中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
// 是否要缓存到硬盘:如果没设置只缓存到内存的选项就需要缓存到硬盘
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// 在单例管理对象SDWebImageDownloader中已经实现了图片的缩放,这里是用于自定义管理对象以避免额外的缩放
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
// 如果当前对象不是SDWebImageManager的单例对象,并且设置里过滤链接代码块,并且下载到了图片。就进行缩放
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// 如果设置了刷新缓存选项,并且有缓存图,并且没有下载图,就什么也不需要做
} 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];
NSData *cacheData;
if (self.cacheSerializer) {
// 如果设置了图片编码代码块,就调用代码块对下载的图片进行编码。
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
// 否则就检查图片是否被转换,如果被转换过,就不获取其二进制数据了,如果没转换过就用原始二进制数据
cacheData = (imageWasTransformed ? nil : downloadedData);
}
// 缓存图片
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
// 进行回调
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
// 如果有下载图并且下载完成
if (downloadedImage && finished) {
if (self.cacheSerializer) {
// 如果设置了图片编码代码块,就在全局并发队列中异步调用代码块对下载的图片进行编码,然后缓存图片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
});
} else {
// 否者就直接缓存图片
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
// 进行回调
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
// 如果下载完成就从操作对象集合中移除
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
} else if (cachedImage) {
// 如果不需要下载并且获取到了缓存图就进行回调然后从操作对象集合中移除
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 如果不需要下载并且也没有缓存图就进行回调然后从操作对象集合中移除
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
// 返回操作对象
return operation;
}
看完了这个方法的实现,我们来大概的梳理一下其逻辑:
- 首先会查看要加载的图片链接是否失败过,再根据设置的选项决定要不要继续加载。
- 然后根据图片链接查找是否有该图片的缓存,再根据设置的选项决定要不要网络下载。
- 接着根据下载的图片是否是动图,再根据设置的选项决定怎么转换动图。
- 最后根据设置的选项进行缓存,并进行回调。
主要的逻辑就是这样,当然里面还有一些具体的细节。
8.获取图片缓存
在图片加载的方法实现中,可以看到有比较重要的两个方法,一个是获取图片缓存,另一个是从网络下载图片。在这一节,我们先看获取图片缓存:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;
点击方法进入查看其实现:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// 如果没传参数key就直接回调并返回,就不继续向下执行了
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 先根据key查找内存中是否有缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果有缓存图片,并且没设置强制从硬盘中查找缓存,就直接回调并返回了
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 生成一个操作对象
NSOperation *operation = [NSOperation new];
// 生成一个查询硬盘缓存的代码块
void(^queryDiskBlock)(void) = ^{
// 如果操作取消就直接返回,不执行回调
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
// 生成一个自动释放池
@autoreleasepool {
// 根据key查找硬盘中是否有缓存
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
// 如果内存中有缓存
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// 如果内存中没有缓存但是硬盘中有缓存
diskImage = [self diskImageForKey:key data:diskData];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
// 如果设置了同步查询硬盘缓存的选项就直接调用,否则就主队列异步回调
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// 如果设置了同步查询硬盘缓存的选项就直接调用,否则就自定义·串行队列异步回调
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
获取图片缓存的逻辑还是很清晰的:
- 首先查找在内存中的缓存,再根据设置的选项决定要不要继续查找。
- 然后根据设置的选项决定是同步还是异步查找硬盘中的缓存。
- 接着根据设置的选项决定要不要把硬盘中的缓存图片缓存到内存中。
- 最后进行回调数据
9.从网络中下载图片
这一节来看一下另一个比较重要的方法,即从网络中下载图片:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
点击方法查看实现,发现这个方法只不过是对另一个方法的封装:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// 这里直接调用了另一个方法,但是我们要看一下传入的createCallback中的内容
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
// 设置下载超时时长,默认为15秒
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 根据设置的选项设置请求缓存策略
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
// 生成请求对象,并设置参数
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
// 生成下载操作的封装对象,并设置参数
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;
}
// 如果选项设置了后进先出(LIFO),就让上一个操作依赖于当前操作
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation is dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
在传入下一个方法的参数createCallback中,主要是生成了一个请求对象request,然后把这个请求对象放到操作对象operation中。如果设置了后进先出(LIFO),还会设置操作依赖。
看完了传入的参数,我们来看看方法中具体做了些什么:
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// 参数url依旧是必传的
if (url == nil) {
// 如果未传的话直接回调并返回
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
// 加锁,其实是利用信号量来实现的串行
LOCK(self.operationsLock);
// 从加载操作对象集合中获取url对应的操作对象
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
// 如果没有获取到url对应的操作对象
if (!operation) {
// 从传入的代码块中或操作对象
operation = createCallback();
// 设置操作对象的完成回调,在回调中把url对应的操作对象从操作对象集合中移除
__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);
};
// 将生成的操作对象以url为key添加到操作对象集合中,
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple is doc.
// 将生成的操作对象添加到操作队列中执行
[self.downloadQueue addOperation:operation];
}
// 解锁
UNLOCK(self.operationsLock);
// 生成一个token,方便对该操作对象进行操作
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
看完这两个方法(或者说是一个),我们发现并没有进行网络请求。这个方法主要做的内容是:
- 先根据传入的
url生成NSMutableURLRequest对象request。 - 再根据上一步生成的对象
request生成一个自定义的继承自NSOperation类的SDWebImageDownloaderOperation对象operation。 - 接着把对象
operation保存到操作对象集合中,并添加到操作队列开始执行。 - 最后根据参数生成一个
SDWebImageDownloadToken对象token返回,以便利用这个token对操作对象做一些操作。
10.真·网络请求
上一节虽然叫“从网络中下载图片”,但是我们从代码中并没有发现发起网络请求的操作,这一节,就来看看下载图片的网络请求到底在哪儿,是怎么实现的。
上一节中我们看到已经生成了一个网络求情对象request,按理来说,想要发起网络请求,只需利用request生成一个NSURLSessionTask对象,然后resume一下就可以。但是对象request并没有生成NSURLSessionTask对象,而是生成了一个作者自定义的类对象——继承自NSOperation类的SDWebImageDownloaderOperation类对象operation。然后直接添加到操作队列执行了。所以这个SDWebImageDownloaderOperation类中一定存在一个大阴谋!
在一个NSOperation类对象开始执行的时候会调用其对象方法- (void)start;,所以我们在自定义类SDWebImageDownloaderOperation中,看到作者重写了这个方法:
- (void)start {
// 加锁
@synchronized (self) {
// 如果已经被设置为取消状态就直接复位,然后返回了
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if SD_UIKIT
// 查看是否能获取到UIApplication类
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
// 如果有UIApplication类,并且设置了进入后台依旧下载的选项
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
// 开启后台下载
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
// 如果后台任务执行结束,就取消任务
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
// 获取传入的网络会话对象
NSURLSession *session = self.unownedSession;
// 如果没有传入网络会话对象
if (!session) {
// 生成一个网络会话对象
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
// 如果设置了忽略从NSURLCache中获取缓存的选项
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// 获取NSURLCache中的缓存,并保存
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
// 生成NSURLSessionTask类对象
self.dataTask = [session dataTaskWithRequest:self.request];
// 设置属性为开始执行
self.executing = YES;
}
// 如果NSURLSessionTask类对象存在
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
// 因为NSURLSessionTask的priority这个属性是iOS8.0以后才有的,所以要判断一下
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
// 根据设置的选项设置优先级
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
// 开始执行任务
[self.dataTask resume];
// 获取到进度回调代码块并调用
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
// 异步主队列发送下载开始通知
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
// 如果没有获取到NSURLSessionTask类对象,就回调错误并返回
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
// 如果是后台任务,就关闭后台任务
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
这就是SDWebImage在从网络加载图片的网络请求方法了,逻辑也是清晰明了:
- 先判断这个操作对象是否已经被取消了,被取消就不继续执行了。
- 再判断是否设置了后台下载选项,进行后台下载设置。
- 接着设置网络会话对象。
- 再根据设置的选项判断是否忽略NSURLCache的缓存。
- 接着根据设置的选项设置任务对象的优先级。
- 然后启动任务,调用进度回调代码块,发送通知。
- 最后如果设置了后台下载选项,就结束后台下载任务。
11.网络请求代理回调
上面所有的实现都是为了发起网络请求,发起请求是一方面,处理请求结果又是另一个方面,这一节主要看的是SDWebImage对网络请求结果的处理。对结果的处理主要集中在任务对象的代理方法中:
11.1.NSURLSessionDataDelegate代理实现
/**
当dataTask接收到初试响应时,会调用这个代理方法
*/
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// 生成变量保存响应处置为允许继续
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
// 生成变量保存响应内容的预期长度,如果没获取到设置为0
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
// 用属性保存响应内容的预期长度
self.expectedSize = expected;
// 用属性保存响应对象
self.response = response;
// 生成变量保存状态码,如果没获取到设为为200
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
// 生成变量保存有效性,如果状态码小于400就有效
BOOL valid = statusCode < 400;
// 如果状态码是304,并且没有缓存数据,也是无效的
if (statusCode == 304 && !self.cachedData) {
valid = NO;
}
if (valid) {
// 如果是有效的就调用进度回调代码块
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
} else {
// 如果是无效的就设置响应处置为取消
disposition = NSURLSessionResponseCancel;
}
// 主队列异步发送通知告知已收到响应
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
});
// 调用完成处理代码块并传递响应处置设置
if (completionHandler) {
completionHandler(disposition);
}
}
/**
当dataTask在接收到数据时,会调用这个代理方法,这个方法在接受数据期间会反复调用
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// 如果保存图片数据的属性为空,就以上面那个代理方法中获取的图片期望大小为容量,初始化这个属性
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
// 将服务器返回的数据添加到属性中保存
[self.imageData appendData:data];
// 如果设置了渐进式下载的选项,并且图片的期望大小大于零
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// 获取到当前获取的图片数据
__block NSData *imageData = [self.imageData copy];
// 获取到当前下载的图片的字节数
const NSInteger totalSize = imageData.length;
// 获取当前图片是否下载完成,如果当前下载的图片的字节数不少于图片期望的字节数就为完成
BOOL finished = (totalSize >= self.expectedSize);
// 如果没有实例化渐进式编码器
if (!self.progressiveCoder) {
// 遍历所有的编码器,如果编码器实现了SDWebImageProgressiveCoder代理,并且能够解码下载的图片数据,就以该编码器的类实例化一个编码器对象
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
self.progressiveCoder = [[[coder class] alloc] init];
break;
}
}
}
// 自定义串行队列异步执行
dispatch_async(self.coderQueue, ^{
// 解码生成图片
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
// 如果生成了图片
if (image) {
// 获取该图片链接地址对应的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 缩放图片
image = [self scaledImageForKey:key image:image];
// 如果压缩图片就压缩
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
// 进行完成回调
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
});
}
// 回调进度
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
/**
当dataTask完成接收所有预期数据后会调用这个代理方法,询问代理对象是否应将响应存储在缓存中。
*/
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
// 获取到缓存响应
NSCachedURLResponse *cachedResponse = proposedResponse;
// 如果用户设置了进行缓存就回调缓存响应,否则就回调nil不缓存
if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
cachedResponse = nil;
}
// 调用完成代码块并传参
if (completionHandler) {
completionHandler(cachedResponse);
}
}
11.2.NSURLSessionTaskDelegate代理实现
/**
当dataTask已经完成传输数据时会调用这个代理方法
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
@synchronized(self) {
// 将保存任务对象的属性置空
self.dataTask = nil;
// 主队列异步发送下载停止通知,如果没有错误就发送下载完成通知
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
}
});
}
if (error) {
// 如果出错就回调错误
[self callCompletionBlocksWithError:error];
// 调用完成方法
[self done];
} else {
// 如果有完成回调的代码块
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
// 如果有下载的图片数据
__block NSData *imageData = [self.imageData copy];
if (imageData) {
// 如果设置了忽略缓存响应选项,并且缓存数据与下载的图片数据相同
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
// 回调nil,并调用完成方法
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
[self done];
} else {
// 在自定义串行队列异步执行
dispatch_async(self.coderQueue, ^{
// 对下载的图片数据进行解码
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
// 获取图地址链接对应的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 对图片进行缩放
image = [self scaledImageForKey:key image:image];
// 生成变量保存图片是否应该解码
BOOL shouldDecode = YES;
if (image.images) {
// 如果是gif图就不解码
shouldDecode = NO;
} else {
#ifdef SD_WEBP
// 如果是webp格式的也不转码
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
if (imageFormat == SDImageFormatWebP) {
shouldDecode = NO;
}
#endif
}
// 如果需要转码
if (shouldDecode) {
// 如果需要解压图片
if (self.shouldDecompressImages) {
// 获取是否需要缩小图片
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
// 对图片进行解压
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
// 获取处理好的图片尺寸
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
// 如果长或宽有一侧为0,就回调错误
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
// 如果长宽都有数据,就完成回调
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
// 调用完成方法
[self done];
});
}
} else {
// 如果没有下载图片数据就回调错误
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
// 调用完成方法
[self done];
}
} else {
// 如果没有完成回调的代码块就调用完成方法
[self done];
}
}
}
/**
当task接收到身份验证时,会调用这个代理方法。
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
// 设置临时变量保存数据
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// 如果验证方式为NSURLAuthenticationMethodServerTrust
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
// 如果没设置允许不可信的SSL证书,就设置处理方式为默认
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
} else {
// 如果设置了允许不可信的SSL证书,就设置验证模式为通过指定证书验证,并生成证书
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
} else {
如果验证方式不是NSURLAuthenticationMethodServerTrust
if (challenge.previousFailureCount == 0) {
// 如果认证的失败次数设置为0次
if (self.credential) {
// 如果有证书,就设置证书,验证模式为通过指定证书验证
credential = self.credential;
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
// 如果没有证书,就设置验证模式为不需要验证证书
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 如果认证的失败次数设置超过0次,就设置验证模式为不需要验证证书
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
// 调用完成代码块并传参
if (completionHandler) {
completionHandler(disposition, credential);
}
}
看完对代理方法的实现,比较主要的逻辑就是对下载完成后的图片数据进行处理:
- 先解码图片数据生成图片。
- 再对图片进行缩放。
- 最后对图片进行解压。
其他的一些逻辑主要是针对选项参数设置的不同,进行了不同的处理。
12.总结
我们看完了一张图片加载的全过程,最后我们再来总结一下:
- 我们在使用
SDWebImage时调用了[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]];这个简单的分类方法,然后就静静的等着图片被设置到UIImageView类对象上。 - 经过一系列调用,我们首先来到
UIView+WebCache分类中,在这个分类中,首先保障了图片加载的唯一性,然后就开始了核心的加载操作。 - 接着就进入了
SDWebImageManager类中,在这个类中,首先去查找是否有缓存,没有缓存的话才去服务器下载。 - 想要查找缓存我们要进入
SDImageCache这个类中,在这个类中,首先去内存中查看是否有对应的缓存,如果没有再去硬盘中查找是否有对应的缓存,但是从硬盘中获取的是图片的数据,要想获得图片还要经历解码、缩放和解压。当然如果都没有缓存的话就去下载。 - 负责下载的是
SDWebImageDownloader这个类,在这个类中,将图片的下载操作封装成了自定义的一个类SDWebImageDownloaderOperation,然后添加到了操作队列中。 - 当操作队列调用这个操作时,会调用操作对象的
- (void)start方法,在重写的这个方法中,生成了任务对象dataTask,并调用resume开始执行任务。 - 因为
SDWebImageDownloaderOperation类遵守了dataTask对象的协议,所以dataTask执行的结果会通过代理方法进行回调。在代理方法中,获取并保存了服务器返回的数据,并在任务执行结束后,对数据进行解码、缩放和解压。处理完成后就进行回调。 - 通过重重回调,要回调的数据沿着
SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache一路流动,其中流动到SDWebImageManager中时对图片进行了缓存,最后在UIView+WebCache中为UIImageView设置了处理好的图片。 - 这就是
SDWebImage加载图片的大体流程,当然还有非常多的小细节和小功能,这些就在以后对具体类的阅读中学习了。
最后附上SDWebImage接口文档的两张图:
源码阅读系列:SDWebImage
源码阅读:SDWebImage(二)——SDWebImageCompat
源码阅读:SDWebImage(三)——NSData+ImageContentType
源码阅读:SDWebImage(四)——SDWebImageCoder
源码阅读:SDWebImage(五)——SDWebImageFrame
源码阅读:SDWebImage(六)——SDWebImageCoderHelper
源码阅读:SDWebImage(七)——SDWebImageImageIOCoder
源码阅读:SDWebImage(八)——SDWebImageGIFCoder
源码阅读:SDWebImage(九)——SDWebImageCodersManager
源码阅读:SDWebImage(十)——SDImageCacheConfig
源码阅读:SDWebImage(十一)——SDImageCache
源码阅读:SDWebImage(十二)——SDWebImageDownloaderOperation
源码阅读:SDWebImage(十三)——SDWebImageDownloader
源码阅读:SDWebImage(十四)——SDWebImageManager
源码阅读:SDWebImage(十五)——SDWebImagePrefetcher
源码阅读:SDWebImage(十六)——SDWebImageTransition
源码阅读:SDWebImage(十七)——UIView+WebCacheOperation
源码阅读:SDWebImage(十八)——UIView+WebCache
源码阅读:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat
源码阅读:SDWebImage(二十)——UIButton+WebCache
源码阅读:SDWebImage(二十一)——UIImageView+WebCache/UIImageView+HighlightedWebCache