SDWebImage是老生常谈的三方,这篇博客算是一个笔记吧,记录下SDWebImage源码相关加载图片流程.
注1: 整体流程基于 SDWebImage 5.0.6 版本.
注2: 本文只对iOS执行流程进行分析.默认会去除 Mac开发的部分(带有 #if SD_UIKIT || SD_MAC).
SDWebImage 整体流程
我们通过官方的这张图可以看出整体流程,我们主要通过分类方法的形式直接接触SDWebImage的执行流程.
SDWebImage内部加载流程层级较多,所以我这里分为 对外流程、SDWebImageManager 内部流程、SDImageCache 内部流程 、SDWebImageDownloader 内部流程 四个模块进行执行代码流转说明.
对外流程
UIImageView + WebCache 或者 UIButton + WebCache
SDWebImage的三方入口通常是UIImageView + WebCache 或者 UIButton + WebCache 这种分类的 sd_setImageWithURL 方法.
- (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT;
不管是哪个分类的 sd_setImageWithURL 将统一进入 UIView + WebCache 的 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;
internalSetImageWithURL 内部流程
- 判断当前视图上是否有正在进行的下载任务,如果有,那么就通过 sd_cancelImageLoadOperationWithKey 方法进行停止,对于其中的参数key,默认是使用类名.
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]);
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey]; // 停止先前的加载任务
self.sd_imageURL = url;
- 通过位运算判断传入参数 options 中是否包含 延时加载展位图(SDWebImageDelayPlaceholder), 如果没有,那么就返回主队列并且设置占位图.
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
- 判断参数 url 是否为nil, 如果为真,那么就通过completedBlock直接返回错误信息.
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 不为nil,那么进入正常的加载流程.
if (url) {
..... 正常流程
} else {
..... 错误流程
}
- 首先对SDWebImageManager 进行懒加载.然后创建进度回调Block(combinedProgressBlock).
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
@strongify(self);
NSProgress *imageProgress = self.sd_imageProgress;
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
- 通过SDWebImageManager的 loadImageWithURL: 创建一个SDWebImageOperation,并且把它添加到当前视图任务HashMap中.这样如果后面会有新的任务,那么就可以通过key找到前次任务并且进行停止操作.
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) {
.... 完成回调
}];
// 把图片加载Operation 加到 NSMapTable中.
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
- 在SDWebImageOperation的completedBlock中主要来进行图片完成的设置.
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
SDWebImageManager 内部流程
SDWebImageManager 中的 loadImageWithURL
- 首先对 url 进行容错处理. 比如你的URL参数传入的是字符串类型,那么会内部转为NSURL类型. 再例如你传入其他类型就会置为nil.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
- 创建一个 SDWebImageCombinedOperation 对象, 这个对象将会关联 Manager Cache 和 Download之间的关系.它有一个很重要的协议方法就是 cancel,用于停止加载图片的任务.
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
- 判断当前 URL 是否是一个失效的URL,例如先前下载过,但是任务失败会加入Manager单例中的集合属性 failedURLs 中. 所以我们判断 当前URL 是否在 failedURLs 这个集合中即可.
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
- 根据上面的几种判断,例如URL错误,URL失效问题.如果成立,那么就在主队列中调用completedBlock 返回错误信息.
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
return operation;
}
- 如果一切正常,那么就把当前 operation 添加到 runningOperations 这个集合属性中.
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);
- 调用 callCacheProcessForOperation 方法,首先进行图片缓存查找.
[self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
SDWebImageManager 中的 callCacheProcessForOperation
- 根据传入配置参数通过位运算判断 当前是否只允许下载模式.
BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
- 如果只是下载模式那么直接调用 callDownloadProcessForOperation 方法进入下载模式.
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
- 如果是需要先查找缓存,那么根据 URL 通过MD5加密算法生成key.然后通过 SDImageCacheManager 单例通过 queryImageForKey 创建查询任务.并把任务Operation 返回赋值给 SDWebImageCombinedOperation 中的 cacheOperation 属性.
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
.... 查询完成回调
}];
- 在查询完成内部主要是判断是否已经查找到图片缓存,图片缓存的查找流程我们等一下再聊.如果图片缓存查找到就安全的移除Operation, 如果没有查找到,那么就进行下载图片操作.
if (!operation || operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
SDWebImageManager 中的 callDownloadProcessForOperation
- 确定是否需要下载图片.依据有 配置选项中是否只有查找缓存的配置, 是否需要重新刷新图片缓存, 有没有实现图片下载的协议方法,url是否允许可以被下载 四个条件. 最终生成条件全部通过按位与的位运算生成的.
BOOL shouldDownload = (options & SDWebImageFromCacheOnly) == 0;
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [self.imageLoader canRequestImageForURL:url];
- 首先我们先说一下,如果不走下载图片流程,那么程序就会判断参数中的 cacheImage 是否为 nil, 如果不为空,那么就会返回缓存图片,所以说 callCacheProcessForOperation 这个方法中不做完成返回的. 如果 cacheImage 是个nil,并且不允许下载代理,那么在 completedBlock 返回 nil.
if (shouldDownload) {
.... 正常下载流程
}else if (cachedImage) {
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
- 正常的图片下载流程,首先判断参数 options 是否有图片缓存配置项(SDWebImageRefreshCached)和是否有缓存图片,如果有,那么就先把缓存图片传递回去.然后再进行图片下载工作.
if (cachedImage && options & SDWebImageRefreshCached) {
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
- 通过SDWebImageDownLoader 单例进行通过 requestImageWithURL 图片网络请求.返回下载的operation,赋值给SDWebImageCombinedOperation对象中的 loaderOperation属性.
@weakify(operation);
operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
.... 完成回调
}];
- 在requestImageWithURL完成回调中, 我们主要对返回结果进行处理,例如 ① 如果下载任务被取消,不进行任何操作; ② 如果是刷新缓存保存,也是不进行任何操作; ③ 如果是普通任务出错,那么就把url放入failedURLs中; ④ 如果完成图片下载任务, 根据配置options 判断是否从移除错误的URL.然后调用 callStoreCacheProcessForOperation 进入图片返回方法. 上述完成之后,安全移除operation.
if (!operation || operation.isCancelled) {
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
} else if (error) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
if (finished) {
[self safelyRemoveOperationFromRunning:operation];
}
SDWebImageManager 中的 callStoreCacheProccessForOperation
该方法是主要是进行缓存存储,缓存存储图片主要有两种形式,一种是通过Transform 处理过的图片,一种是普通的图片形式.
- 首先进行transformer的对象取出以及生成 需要存入的key.
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
- 如果是transform转换的图片,那么会在全局异步并发队列中进行转换工作,然后通过 SDImageCacheManager 单例 storeImage 方法进行缓存存储.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key];
if (transformedImage && finished) {
NSString *transformerKey = [transformer transformerKey];
NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey);
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url];
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil];
}
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
});
- 如果不满足条件,那么就进行普通的存储.存储完成则通过callCompletedBlockWithOperation
if (downloadedImage && finished) {
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:storeCacheType completion:nil];
}
});
} else {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:storeCacheType completion:nil];
}
}
[self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
SDImageCache 内部流程
对于 SDImageCache 的流转入口主要是 queryImageForKey 获取图片 以及 storeImage 存储图片两个入口,我们一一来看.
SDImageCache 的 queryImageForKey
- 方法首先对配置项进行操作转换 由 SDWebImageOptions 转为 SDImageCacheOptions.整体代码如下所示.
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
- 进入 queryCacheOperationForKey 方法.
return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
SDImageCache 的 queryImageOperationForKey
- 首先先验证key,如果key为nil,直接block返回结果.
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
- 如果有transform操作,那么就对key进行处理操作.
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}
- 从 SDMemoryCache 取出 图片缓存,对于 SDMemoryCache, 后面我们将一起看一下他的具体内部流程.
UIImage *image = [self imageFromMemoryCacheForKey:key];
- 判断当前图片是否是GIF动图,并且判断配置项中是否含有只对图片第一帧进行解码操作(SDImageCacheDecodeFirstFrameOnly).如果条件成立,就取出图片的第一帧并且返回.
if ((options & SDImageCacheDecodeFirstFrameOnly) && image.sd_isAnimated) {
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
}
- 判断只查询缓存中的图片数据,如果是,那么就调用完成block 直接返回数据.
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
- 如果上方没有返回,那么就查询沙盒图片数据,这里首先创建一个Operation对象,并且判断需要异步查询还是同步查询.
NSOperation *operation = [NSOperation new];
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
- 创建查询沙盒数据Block.
void(^queryDiskBlock)(void) = ^{
};
- 查询沙盒数据Block主要是通过key来进行查询的.整个过程是在 @autoreleasepool 中进行的,所以能保证临时的数据对象及时释放.
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
- 根据情况判断返回图片, ① 如果缓存中有图片数据,那么就直接启用缓存图片; ② 如果缓存中没有图片数据并且沙盒中有图片数据,那么首先先对图片进行解码操作.因为默认的图片结果过程只是在CPU提交之前才会进行解码,提前解码可以有效的减少卡顿. 然后再把图片存入缓存中.
if (image) {
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
- 判断是异步执行还是同步执行,如果是异步执行,通过GCD返回主线程然后返回数据.
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
- 查询沙盒的Block创建完成之后,根据配置项在串行队列中异步或者同步执行.
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
SDImageCache 的 storeImage
storeImage方法主要是用来存储图片缓存,根据参数进行缓存存储和沙盒存储.
- 缓存存储数据比较简单,具体如下所示.
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memCache setObject:image forKey:key cost:cost];
}
- 沙盒存储主要通过异步串行队列进行存储操作.
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
SDImageFormat format;
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
SDWebImageDownLoader 内部流程
requestImageWithURL
- 首先根据SDWebImageOptions 转换为 SDWebImageDownLoadOptions的配置.
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
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 (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
if (cachedImage && options & SDWebImageRefreshCached) {
downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
- 调用 downloadImageWithURL 进行图片下载.
return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
downloadImageWithURL
- 首先仍然是判断 url 是否为nil, 如果为nil,那么就直接返回.
if (url == nil) {
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, YES);
}
return nil;
}
- 如果保证相同的url同时进行图片加载,只有一个下载任务呢?首先SDWebImageDownloader 中 有个字典URLOptions 用来存储当前所有的下载任务,key就是URL. 再就是每一次下载任务都是 在加锁条件下(信号量) 进行的,以前的两个操作就可以解决先前的问题.具体操作如下所示.
SD_LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished || operation.isCancelled) {
.... 相关操作
}
SD_UNLOCK(self.operationsLock);
- 在创建的具体操作,主要是创建下载任务. 如果创建失败就会进行解锁通过completedBlock返回错误. 再就是会对任务完成的completedBlock进行赋值,Block里面主要是从URLOperations 中移除任务. 最后添加到 任务队列中 和 URLOperation 中.
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
if (!operation) {
SD_UNLOCK(self.operationsLock);
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
completedBlock(nil, nil, error, YES);
}
return nil;
}
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
SD_LOCK(self.operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self.operationsLock);
};
self.URLOperations[url] = operation;
[self.downloadQueue addOperation:operation];
- 上面说到一个问题是 同一时间 相同URL 只会创建一个Operation,那么回调completedBlock 和 进度progressBlock, SDWebImage又是如何管理的呢? 这个主要是存放在 SDWebImageDownloaderOperation 中的 数组属性 callBackBlocks, 数组中存放着字典 以 kProgressCallbackKey 和 kCompletedCallbackKey 为 键,存放着 完成 和 进度 Block. 外部入口调用如下所示.
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
- 接着就是创建Token信息和返回Token.
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
token.downloader = self;
return token;
到此,下载流程基本结束.
SDMemoryCache的相关
SDMemoryCache 是继承于 NSCache, NSCache具有线程安全,会根据内存警告自动做删减操作,不拷贝键等特点.
SDMemoryCache 含有一个NSMapTable类型的弱引用哈希表,这是为什么呢? 主要是因为 NSCache如果因为内存不足就会清理时,假设对象还没有被销毁,那么我们可以通过弱引用表找到该对象,增加了Cache的广度.
关于SDWebImage的提前对图片解码操作
在 SDImageCache 从沙盒读取出来的图片数据并不是直接存入缓存中,而是相对图片进行解码操作.这样可以减少后期CPU的计算工作.对提高性能有很大帮助.图片的解码工作主要是通过SDImageCoderHelper 类中的 decodedImageWithImage 和 decodedAndScaledDownImageWithImage 方法完成的.具体操作如下所示.
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (![self shouldDecodeImage:image]) {
return image;
}
CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
if (!imageRef) {
return image;
}
UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(imageRef);
decodedImage.sd_isDecoded = YES;
decodedImage.sd_imageFormat = image.sd_imageFormat;
return decodedImage;
}
关于SDWebImage的多线程问题
在 SDWebImage 的线程安全上基本上是用了信号量来实现线程安全.这个到处可见.如下所示.
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
对于沙盒操作实际上是放在一个串行队列中.这样可以保证访问任务的顺序执行.对于异步还是同步主要是配置项的具体配置.
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
总结
SDWebImage 好几年前就翻了源码,但是只是简单的看了看,没有做记录,现在就接着面试的机会做个记录吧,如果有问题,欢迎指导~ 骚栋谢谢各位大佬了.