SDWebImage在github上有19k+星星。这个库提供了一个支持缓存的异步图像下载器。提供UIImageView和UIButton分类,开发者只需要简单的调用公共接口,就可以实现强大的图片异步下载及缓存的功能。
以下内容,从UIImageView+WebCache
分类公共接口开始,慢慢延伸到方法的具体实现,从中学习了作者的图片下载缓存策略。
UIImageView+WebCache公共接口
- (void)sd_setImageWithURL:(nullable NSURL *)url
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
- (void)sd_setImageWithURL:(nullable NSURL *)url
completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
上面的接口方法内部都是调用的下面这个方法,该方法是在UIView+WebCache分类方法里,方便UIButton和UIImageView都能调用
- (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
context:(nullable NSDictionary *)context
接下来看一下方法内部的具体实现
- (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 *)context {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey]; // 从operationDictionary中找到validOperationKey对应的操作,从队列中取消该操作并移除。保证当前没有正在进行的异步下载操作。
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 设置关联对象url
if (!(options & SDWebImageDelayPlaceholder)) { // 如果 没有 设置延迟添加占位图片选项
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) { // url存在
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) { // 检查活动指示器视图是否启用
[self sd_addActivityIndicator]; // 添加活动指示器视图,并让活动指示器动画
}
SDWebImageManager *manager;// 如果有自定义的管理者,则用自定义的管理者,否则,用单例管理对象
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
__weak __typeof(self)wself = self; // 用__weak修饰,防止循环引用
// 用SDWebImageManager下载图片,并用operation接收返回值,operation是遵守SDWebImageOperation协议的操作, 该协议只有一个取消方法.
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { // 下载完成回调
__strong __typeof (wself) sself = wself; // 用__strong修饰,防止self被释放
[sself sd_removeActivityIndicator]; // 移除活动指示器
if (!sself) { return; } // 保证下面的代码安全
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); // 是否应该调用完成回调,下载完成或者设置了避免自动设置图片选项是为YES
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
// 是否应该不设置图片, 下载图片存在并且设置了避免自动设置图片选项 或者 下载图片不存在并且没有设置延迟加载占位图选项时 为YES;
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
if (shouldNotSetImage) { // 如果不设置图片
dispatch_main_async_safe(callCompletedBlockClojure); // 主线程调用完成回调
return;
}
UIImage *targetImage = nil; // 目标image
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
// 下载好了图片并且没有设置避免自动设置图片选项
targetImage = image; // 将下载好的图片设置为目标image
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
// 图片不存在并且设置了延迟加载占位图选项
targetImage = placeholder; // 将占位图设置为目标image
targetData = nil;
}
if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) { //如果 没有 设置延迟加载占位图片选项 才会有调度组
dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey]; // 获取调度组
dispatch_group_enter(group); // 进入组
dispatch_main_async_safe(^{ // 安全回到主线程异步处理
// 设置目标图片
// 如果有setImageBlock,则不会设置目标图片,而是用setImageBlock回调给开发者自己去设置
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
// ensure completion block is called after custom setImage process finish
// 确保完成的block调用是在自定义设置图片过程完成之后
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
callCompletedBlockClojure();
});
} else { // 没有调度组,即设置了延迟加载占位图
dispatch_main_async_safe(^{
// 设置目标图片
// 如果有setImageBlock,则不会设置目标图片,而是用setImageBlock回调给开发者自己去设置
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 完成回调
callCompletedBlockClojure();
});
}
}];
// 将operation操作与validOperationKey关联起来,作为键值对保存在operationDictionary字典中,表示当前的操作正在进行
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else { // url == nil
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator]; // 移除活动指示器
if (completedBlock) { // 回调错误信息
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
该方法的作用:
- 避免多个异步加载任务同时存在
- 根据需要设置临时占位图
- 活动指示器在合适的时机显示与隐藏
- 调用
SDWebImageManager
来加载图片 - 将下载操作存放到正在进行的操作字典中
其中从队列中取消正在下载的操作内部实现如下:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; // 获取操作字典
id<SDWebImageOperation> operation; // 操作
@synchronized (self) { // 加线程锁 保证线程安全
operation = [operationDictionary objectForKey:key]; // 根据key 取出操作
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]){
[operation cancel]; // 取消此操作
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key]; // 移除key
}
}
}
SDWebImageManager
加载图片的内部实现如下:
// 加载图片
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
// 如果传入的url是NSString类型,就转化为NSURL类型。
// 很常见的错误是,开发者将NSString对象代替NSURL对象作为URL传递,出于一些奇怪的原因,Xcode不会为这种类型的不匹配而抛出警告。以下方法允许url作为NSString类型来传递
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 确保url正确,防止app在参数类型错误时崩溃,例如传递了一个NSNull代替NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 组合操作
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO; // url是否失效的标记
if (url) { // 如果url存在
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url]; // 失效url名单中若包含此url 则标记为YES
}
}
// url 的绝对字符串为0 或者 (没有设置重置失败选项 并且 isFailedUrl为YES) 则直接调用完成的操作并返回错误信息
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;
}
// url有效
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation]; // 将操作添加到正在进行的操作数组中
}
// 根据url返回对应的缓存key
NSString *key = [self cacheKeyForURL:url];
// 组合操作的缓存操作, 根据缓存key获取(使用self.imageCache查询是否存在缓存图片)
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) { // 如果操作被取消
// 安全从正在进行下载操作数组中移除此操作
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 如果(没有缓存图片或者设置了重置缓存选项) 并且 (代理对象没有响应mageManager:shouldDownloadImageForURL:方法默认为YES或者代理对象响应该方法时返回YES --》 需要下载图片
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 存在缓存图片 并且需要重置缓存选项(有缓存图片也要下载更新图片)
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.
// 如果在缓存中找到图片,但设置了SDWebImageRefreshCached选项,会通知缓存的图片,并尝试重新下载,以便让NSURLCache从服务器刷新它
// 回调缓存图片然后重新下载
[self callCompletionBlockForOperation:weakOperation 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
// 如果没有图片或任何请求属性,并被代理允许下载,则去下载图片
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
// 如果缓存图片存在并强制刷新缓存,则强制关闭progressive
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
// 忽略从NSURLCache读取的图像
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 使用图片下载器下载
//subOperationToken用来标记当前的下载任务,便于被取消
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 操作被取消则不做任何处理, 避免和其他的completedBlock重复
// 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:strongOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url]; // 将url添加到记录失效url的名单中
}
}
}
else { // 下载成功
if ((options & SDWebImageRetryFailed)) { // 设置了 下载失败后尝试重新下载选项,
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url]; // 从失效url名单中移除url
}
}
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.
// 如果不是共享管理器并有缓存key过滤和下载的图片,缩放图片
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
} 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];
// pass nil if the image was transformed, so we can recalculate the data from the image
// 缓存存储被处理的图片,如果已经处理了图片,则imageData设为nil,因此可以重新计算图片数据
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
// 将图片传入完成回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
// 不需要处理图片
if (downloadedImage && finished) {
// 将下载好的图片保存到缓存中
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
// 再执行完成回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
// 下载完成 从正在下载的操作数组中移除本次下载操作
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
@synchronized(operation) {
// Need same lock to ensure cancelBlock called because cancel method can be called in different queue
// 需要线程锁才能确保cancelBlock被调用,因为可以在不同的队列中调用cancel方法
operation.cancelBlock = ^{
// 取消下载任务
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 从正在下载的操作数组中移除本次下载操作
[self safelyRemoveOperationFromRunning:strongOperation];
};
}
} else if (cachedImage) { // 有缓存图片
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 使用缓存图片回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 从正在下载的操作数组中移除此操作
[self safelyRemoveOperationFromRunning:operation];
} else { // 缓存中没有图片并且没有下载到图片
// Image not in cache and download disallowed by delegate
// 回调图片为nil
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
// 从正在下载的操作数组中移除此操作
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
}
SDWebImageManager
加载图片的过程:
- 确保url传递正确
- 对失效url的处理
- 根据key从缓存中查找对应的缓存图片
3.1 如果有缓存图片则使用缓存图片
3.2 如果没有缓存图片并且没有下载到图片则图片为nil
3.3 没有缓存图片但下载成功的处理
3.3.1 如果只做内存缓存,则把下载好的图片保存到内存中
3.3.2 否则,将下载好的图片先保存到内存中,再将图片数据保存到磁盘中。
3.3.3 回调使用下载图片
查询缓存图片方法内部实现如下:
// 根据缓存key 现在内存中查询图片,如果存在,则用内存中的图片执行回调,否则,去磁盘缓存中查找图片,如果找到了,并且需要设置为内存缓存,则先保存到内存缓存中,然后用磁盘缓存图片执行回调
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// 如果key不存在,有回调则直接回调 没有图片缓存
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
// 首先根据key在内存缓存中查询图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果图片存在
if (image) {
NSData *diskData = nil;
// 如果是动画图片,则根据key获取磁盘数据,后面传到完成的回调中,如果不是就传nil
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) { // 有完成的回调调用回调 内存图片,磁盘数据,缓存类型:内存缓存
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
// 内存缓存中没有找到图片
NSOperation *operation = [NSOperation new]; // 创建操作
dispatch_async(self.ioQueue, ^{ // 在串行ioQueue队列中异步执行block操作
if (operation.isCancelled) {
// do not call the completion if cancelled
// 如果操作被取消,则不调用完成的回调
return;
}
@autoreleasepool {
// 根据key获取磁盘数据
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
// 根据key在磁盘中查找图片
UIImage *diskImage = [self diskImageForKey:key];
// 如果图片存在 并且 配置了应该在内存中缓存图片
if (diskImage && self.config.shouldCacheImagesInMemory) {
// 获取图片的内存成本
NSUInteger cost = SDCacheCostForImage(diskImage);
// 在内存中缓存图片
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 有完成回调
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程中异步执行 执行回调 磁盘图片,磁盘内存,缓存类型为磁盘缓存
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
return operation;
}
查找缓存图片逻辑:
先从内存缓存中查找缓存图片,如果有就使用内存缓存图片,否则去磁盘缓存中查找缓存图片,如果找到了,并配置了应该在内存中缓存图片选项,就先保存到内存缓存中,然后再使用缓存图片。
将下载好的图片保存到缓存的方法实现如下:
// 缓存下载图片
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
// 图片不存在 或者 key不存在, 直接执行完成block
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
// 如果配置了启用内存缓存
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image); // 获取图片成本
// 将图片添加到内存缓存中
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) { // 如果没有标记只做内存缓存,则需要进行磁盘缓存
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData; // 获取图片数据
if (!data && image) { // 存在图片但数据不存在
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
// 如果我们没有任何数据检测图像格式,请检查它是否包含alpha通道以使用PNG或JPEG格式
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) { // 包含alpha通道的为PNG格式否则为JPEG格式
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
// 将图片按照对应格式编码为数据类型
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
// 将图片数据保存到磁盘中
[self storeImageDataToDisk:data forKey:key];
}
// 执行保存完成回调
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
存储下载图片到缓存的逻辑: 如果配置了启用内存缓存,则先保存到内存中,如果没有标记只做内存缓存,则需要保存到磁盘中,那么就将图片按照对应格式转换为NSData类型存储到磁盘文件里。
使用SDWebImageDownloader下载图片内部实现:
// 下载图片
- (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;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0; // 默认下载超时时间15.0秒
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
// 为了防止潜在的重复缓存(NSURLCache + SDImageCache),我们禁用图像请求的缓存
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.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
// 创建下载操作
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential; // 设置url凭证
} 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;
}
// 将操作添加到下载队列中
[sself.downloadQueue addOperation:operation];
//如果是LIFO - 后进先出
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
// 通过系统地添加新操作作为最后一个操作的依赖关系来模拟LIFO执行顺序
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
保存progressBlock方法内部实现:
// 保存progressBlock
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
// 如果url不存在,立即执行完成的block
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url]; // 获取下载操作
if (!operation) { // 操作若不存在则创建一个下载操作
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
// 下载操作完成,从操作字典中移除此操作
[self.URLOperations removeObjectForKey:url];
};
});
};
}
// 保存progressBlock和completedBlock
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
总结:
SDWebImageManager
同时管理SDImageCache
和SDWebImageDownloader
,负责协调加载图片时缓存图片和下载图片的任务。
加载图片大体工作流程是: 首先查询是否存在缓存,如果有缓存,则使用缓存的图片;否则,使用SDWebImageDownloader
下载器来下载图片,下载成功后,先保存到缓存里,再显示图片。
SDImageCache
管理缓存的查询和存储。查询时: 先看内存中有没有缓存图片,如果有,就用内存缓存图片,若内存中没有,就去磁盘中查找,如果磁盘中有缓存图片,就先保存到内存中,再使用缓存图片。若内存和磁盘中都没有缓存图片,则表示缓存中没有该查找的图片。需要使用SDWebImageDownloader
去下载。SDWebImageDownloader
下载好了之后,先存储图片到缓存。存储时:先保存到内存中,然后保存到磁盘中。
可借鉴的方法
- 用于保证异步执行的线程安全代码如:
dispatch_main_async_safe(^{ // 保证在主线程中执行代码
// 添加临时的占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
其中dispatch_main_async_safe
的宏定义是这样的:
#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);\
}
#endif
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
- 在使用多线程时,对数组或字典的操作需要加线程锁,以保护数据安全。
例如:
在- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
方法中根据key从正在下载的操作字典中获取对应的操作时:@synchronized (self) { // 加线程锁 保证线程安全 operation = [operationDictionary objectForKey:key]; // 根据key 取出操作 }
在
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock
方法中将操作添加到运行操作数组中时:@synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; // 将操作添加到正在进行的操作数组中 }