目录
- 优点
- 位运算枚举(NS_OPTIONS)
- 基本流程
- 核心代码
记得当年对SDWebImage这个库,会用、然后把从内存到磁盘缓存过程说一下就能轻松征服一个面试官,现在爱上了读源码,把大神们的思路及想法整理一下,很有收获。
篇幅有限,本文只从流程的角度解读一下源码
一、SDWebImage优点
如果你不读源码,很少有人会去关心SD里面还有这么多很优的操作,例如:
- 通过runtime动态绑定key,防止重复下载
- url失效处理等
以前写过一个关于动态绑定的文章,浅谈runtime关联,初学者可以看一下。
二、位运算枚举(NS_OPTIONS)
SDWebImage库里面有大量的NS_OPTIONS枚举,判断的时候关于NS_OPTIONS的二进制运算也用到了很多,所以如果不了解这些的话,有些逻辑还是不甚明了的。
在这里做一下简单介绍
typedef NS_OPTIONS(NSUInteger, OptionType) {
OptionType1 = 0, // 二进制 0000, 十进制 0
OptionType2 = 1 << 0, // 0001, 1
OptionType3 = 1 << 1, // 0010, 2
OptionType4 = 1 << 2 // 0100, 4
};
/*
* << >> | & 这些运算相信在大学C语言都学过,这里不做过多介绍
*
* 1、将两个NS_OPTIONS枚举进行 “|” 运算,得到的结果肯定包含这两个枚举类型(option)
* 2、将运算得到的枚举(option)与被比较枚举(OptionType3)值做 “&” 运算,即可得到option是否包含(OptionType3)
*
* 原理:
option = OptionType1 | OptionType2 0001
0010
= 0011
* 这样无论用OptionType1还是OptionType2再和option进行 "|" 运算,
得到的结果都是非零,可用于 option 是否包含着两个枚举值的判断
*/
OptionType option = OptionType1 | OptionType2;
if (option & OptionType3)
{
NSLog(@"option 包含 OptionType3");
}else
{
NSLog(@"option 不包含 OptionType3");
}
NSLog(@"============================");
// 增加枚举选项
option = option | OptionType4;
if (option & OptionType4)
{
NSLog(@"option 包含 OptionType4");
}else
{
NSLog(@"option 不包含 OptionType4");
}
NSLog(@"============================");
// 减少枚举选项
option = option & (~OptionType4);
if (option & OptionType4)
{
NSLog(@"option 包含 OptionType4");
}else
{
NSLog(@"option 不包含 OptionType4");
}
2018-04-27 10:31:04.946240+0800 位运算枚举[1938:69662] option 不包含 OptionType3
2018-04-27 10:31:04.946394+0800 位运算枚举[1938:69662] ============================
2018-04-27 10:31:04.946506+0800 位运算枚举[1938:69662] option 包含 OptionType4
2018-04-27 10:31:04.946602+0800 位运算枚举[1938:69662] ============================
2018-04-27 10:31:04.946709+0800 位运算枚举[1938:69662] option 不包含 OptionType4
三、基本原理
1、显示placeholderImage
2、SDImageCache从缓存中查找图片是否已经下载
3、先从内存图片缓存查找是否有图片
4、如果内存中有图片缓存,显示图片
5、如果内存中没有,生成NSInvocationOperation添加到执行队列开始从硬盘查找图片缓存
6、如果硬盘中有,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存),显示图片
7、如果硬盘中没有,说明该图片没有缓存,需要下载图片,共享或重新生成一个下载器SDWeb-
-ImageDownLoader开始下载图片
8、开始图片网络请求,下载数据
9、数据下载完成后交给SDWebImageDecoder做图片解码
10、回调展示图片
11、图片保存到硬盘缓存和内存缓存
12、SDImageCache初始化会注册一些通知,在内存警告或退到后台的时候清理内存图片缓存,
-应用结束的时候清理过期图片
3.1、常用入口
/** 常用加载方法 */
[self.giftImageView sd_setImageWithURL:[NSURL URLWithString:model.roomPic]
placeholderImage:[UIImage imageNamed:@"public_image_placeholder"]];
/** 在加载图片过程中可获取图片的下载进度和加载成功与否 */
[self.giftImageView sd_setImageWithURL:[NSURL URLWithString:model.roomPic]
placeholderImage:[UIImage imageNamed:@"public_image_placeholder"]
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
}];
/** 下载图片 */
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager.imageDownloader downloadImageWithURL:[NSURL URLWithString:model.img]
options:SDWebImageDownloaderHighPriority
progress:nil
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
}];
3.2、取消下载操作
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
{
/*
* 从队列中取消正在进行的下载程序
*
* 获取添加在UIView的自定义属性
*/
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
// 实现SDWebImageOperation协议
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]){
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
3.3、核心方法
- url判断
- 创建操作operation,并将其添加到运行操作队列中
- 磁盘缓存、下载
/**
下载核心方法
@param url 图片url
@param options 下载方式
@param progressBlock 加载进度block
@param completedBlock 加载结果block
@return SDWebImageOperation
*/
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
}
/*
* 容错处理
*
* 1、如果不传入加载结果回调block,则报错(⚠️:下载为什么不回调结果)
* 2、如果传入的是NSString类型,则转换为url类型
* 3、如果传入的不是url不是NSURL类型则将url置空
*/
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
/*
* 创建SDWebImageCombinedOperation
*/
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
/*
* 判断当前url是否在错误集合中
*
* self.failedURLs, 错误URL集合,如果当前url下载出错,则将其添加到次错误集合中,下次不再进行操作
* @synchronized, 创建互斥锁,保证线程安全
*/
BOOL isFailedUrl = NO;
if (url)
{
@synchronized (self.failedURLs)
{
isFailedUrl = [self.failedURLs containsObject:url];
}
}
/*
* 错误回调条件
* 1、如果url为空
* 2、检测options中是否包含SDWebImageRetryFailed(详情见:二、位运算枚举)
* 3、url在failedURLs错误数组中
*/
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;
}
// 将operation添加到任务数组中,(此操作需加@synchronized保护线程安全)
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
磁盘缓存逻辑、下载
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;
}
/*
* Check whether we should download image from network
* 检查是否应该从网络下载图像
*
* 1、缓存中没有找到image图片 (SDWebImageFromCacheOnly)
* 2、图片需要刷新 (SDWebImageRefreshCached)
* 3、下载代理存在并且执行
*/
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.
*
* 如果该图像在缓存中,则回调这个需要传递的image,并且通知让NSURLCache重新进行缓存
*/
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
/*
* download if no image or requested to refresh anyway, and download allowed by delegate
*
* 如果没有请求图片或者请求刷新,则下载该图片并且调用delegate
*/
//
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
/*
* force progressive off if image already cached but forced refreshing
*
* 如果图像已缓存但强制刷新,则强制累进关闭
*/
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
/*
* ignore image read from NSURLCache if image if cached but force refreshing
*
* 如果图像缓存但强制刷新,则忽略NSURLCache中读取的图像
*/
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
/*
* 创建下载操作
*
*
*/
__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)
{
/*
* 取消操作
*
* Do nothing if the operation was cancelled
* See #699 for more details
*
* if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
*/
} else if (error)
{
/*
* 回调
*
*/
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
/*
* Check whether we should block failed url
*
* 检测url下载是否失败
*/
//
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)])
{
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else
{
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);
}
/*
* 将失败的url添加到失败url数组中
*/
if (shouldBlockFailedURL)
{
@synchronized (self.failedURLs)
{
[self.failedURLs addObject:url];
}
}
}
else {
/*
* 如果设置了下载失败重试,则将url从失败的url数组中移除
*/
if ((options & SDWebImageRetryFailed))
{
@synchronized (self.failedURLs)
{
[self.failedURLs removeObject:url];
}
}
/*
* 如果在缓存中找到了SDWebImageCacheMemoryOnly
*/
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage)
{
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage)
{
/*
* Image refresh hit the NSURLCache cache, do not call the completion block
*
* 图片刷新遇到了NSSURLCache中有缓存的状况,不调用完成回调。
*/
} 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), ^{
/*
* 调用代理方法
* 获取transform以后的图片
*/
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer)
{
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else
{
cacheData = (imageWasTransformed ? nil : downloadedData);
}
/*
* 缓存transform之后的图片
*/
[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
{
/*
* Image not in cache and download disallowed by delegate
* 主线程执行完成回调
*/
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
/*
* 从正在执行的操作列表中安全移除组合操作
*/
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
生成下载器下载
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
/*
* 下载时间默认为15s
*/
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
/*
* In order to prevent from potential duplicate caching (NSURLCache + SDImageCache)
we disable the cache for image requests if told otherwise
*
* 为了避免 NSURLCache 和 SDImageCache 同时缓存,默认不允许image对象的NAURLCache对象
*/
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
/*
* 设置cookies缓存
*/
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
/*
* 默认情况请求和相应是顺序的 请求--->相应
*
* HTTPShouldUsePipelining 属性值为yes,则允许不必等到respone就可以再次请求,可以大大提高网络请求的效率
*/
request.HTTPShouldUsePipelining = YES;
/*
* 设置请求头
*/
if (sself.headersFilter)
{
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}else
{
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
/*
* SDWebImageDownloaderOperation 继承 NSOperation
*
* shouldDecompressImages 是否压缩返回的图片
*/
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
/*
* 指定验证信息
*
* urlCredential : SSL验证
* Basic验证
*/
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;
}
// 设置下载的顺序
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder)
{
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
缓存查找核心方法
- queryCacheOperationForKey
- cachedImageExistsForURL(通过SDImageCache内的方法进行检测)
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock
{
// 1、检测key
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 2、从内存中查找,如果找到image,直接doneBlock回调
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) = ^{
// 如果该线程已经取消操作,直接return
if (operation.isCancelled){
return;
}
// 使用内存池及时释放资源
@autoreleasepool {
//3、如果内存中不存在该图片,则在磁盘中查找
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
// 4、如果在内存中已存在
if (image)
{
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData)
{
/*
* decode image data only if in-memory cache missed
* 如果图片在磁盘中找到则,将其缓存到内存中
*/
diskImage = [self diskImageForKey:key data:diskData];
if (diskImage && self.config.shouldCacheImagesInMemory)
{
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
// 回调用block结果
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 {
// 在ioQueue中串行处理所有磁盘缓存
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}