SDWebImage 是用于下载和缓存图像的顶级库,是目前使用最广泛的第三方图片处理库。它不仅能够异步加载网络图片,还提供了一套图片缓存管理机制,功能非常强大。作者是 Olivier Poitrey,法国视频分享网站 Dailymotion的 CTO,拥有多个开源项目。
我们在解读SDWebImage一文中简单总结了一些 SDWebImage 的相关原理,今天我们从源码入手,从底层进一步分析 SDWebImage 的原理。
SDWebImage
本文针对SDWebImage v5.1.0
版本进行分析,各位童鞋可以SDWebImage获取源码。还可以从SDWebImage 5.0迁移指南中查看新版本都增加了哪些新功能。SDWebImage中还提供了很多流程图来展示 SDWebImage 的类、API等,大家可以点击查看,这里不做赘述。下面开始源码分析。
SDWebImage提供一个UIImageView的分类 UIImageView+WebCache
,里面提供了大量以sd_setImageWithURL:
开头的方法,用来加载和缓存图片资源。这些方法内部最终都会调用
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
然后sd_setImageWithURL:()
内部调用
- (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;
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 {
context = [context copy]; // copy to avoid mutable object
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]);
}
self.sd_latestOperationKey = validOperationKey;
//通过key - value 的形式找到之前的加载操作,将之前的加载操作取消
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;
//设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
if (url) {
// reset the progress
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// check and start image indicator
[self sd_startImageIndicator];
id imageIndicator = self.sd_imageIndicator;
#endif
//获取 SDWebImageManager
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
@weakify(self);
// 通过 SDWebImageManager 类来进行图片加载
id operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
@strongify(self);
if (!self) { return; }
// if the progress not been updated, mark it to complete state
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// check and stop image indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
//SDWebImageAvoidAutoSetImage,默认情况下图片会在下载完毕后自动添加给imageView
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!self) { return; }
//shouldNotSetImage = NO
//如果我们不想下载完图片后直接显示或者想在设置图片之前加一些图片的处理
//会标志Layout,告诉RunLoop先不要加载图片
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
//如果我们没有设置不要直接加载图片或者没有设置延迟设置占位图,就会设置图片
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, 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
//我们得到了一个图像,但设置了SDWebImageAvoidAutoSetImage标志
//我们没有图像,并且没有设置延迟加载占位图
//就会调用callCompletedBlockClojure回调
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = self.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
//更新图片
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
callCompletedBlockClojure();
});
}];
// 将 operation添加到 operation字典中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
// url为nil 则直接进行错误回调提示
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);
}
});
}
}
sd_internalSetImageWithURL:()
函数作为加载图片的入口,在每次再加载图片之前, 都会调用 sd_cancelImageLoadOperationWithKey:()
函数用于取消上一个url
的加载操作, 可以保证展示和加载的图片地址是最新的。
我们进入sd_cancelImageLoadOperationWithKey:()
函数查看实现:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// SDOperationsDictionary是一个字典类型, key是当前视图的字符串, value是视图加载的操作
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id operation;
// 通过key获取到加载操作
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
// 将操作取消掉
if (operation) {
// operation遵守协议
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
// 并从字典中将操作移除
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
经过上面分析我们知道,SDWebImage内部是通过SDWebImageManager
这样一个管理类来加载图片的,下面我们分析一下SDWebImageManager
:
SDWebImageManager
SDWebImageManager是一个单例管理类,是绑定显示图像的视图、图像下载、图像缓存3者的管理类,将一个下载器和一个图片缓存绑定在一起。
SDWebImageManager内部声明了一个id
类型的 delegate
属性,
在delegate中有这样一个方法:
/**
* Controls which image should be downloaded when the image is not found in the cache.
*
* @param imageManager The current `SDWebImageManager`
* @param imageURL The url of the image to be downloaded
*
* @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
*/
// 缓存中没有图片的话是否下载这张图片,如果返回NO的话,则如果缓存中没有这张图片,也不会去重新下载
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nonnull NSURL *)imageURL;
如果我们想要设置只加载缓存中的图片,不加载网络图片,可以遵守这个协议,自己实现加载逻辑。
下面我们来看下:SDWebImageManager的loadImageWithURL: options: progress: completed
加载图片源码具体实现:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull 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.
// 如果是NSString类型则进行强转NSurl
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止应用程序在参数类型错误上崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 自定义Operation对象(继承NSObject) 内部有一个 NSOperation对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
// 检查是否为Url是否是失效的
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// 如果url为空或者是失效的,则直接block回调,并附加上错误信息,不再进行加载操作
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;
}
// 将当前操作 添加 到正在运行的操作 集合中
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);
// Preprocess the options and context arg to decide the final the result for manager
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// Start the entry to load image from cache
// 开始在缓存中查询图片
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
在函数内部会创建一个 SDWebImageCombinedOperation
对象,这个对象用于缓存查询和下载操作,如果operation.isCancelled
为YES
,则回调操作会直接返回,不在继续执行;
函数最后会调用了callCacheProcessForOperation:url:options:context:progress:completed:
用于查询图片缓存:
// 开始在缓存中查询图片
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Check whether we should query cache
// 判断是否应该从缓存中查询图片
BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
if (shouldQueryCache) {
id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
// 并在缓存中查询图片,如果内存中有,直接回调, 内存中没有,则会创建NSoperation操作,异步在 磁盘查询
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
// 没有图片缓存或者操作被取消了
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
// 从正在执行的操作中移除,然后返回
[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];
}];
} else {
// Continue download process
// 缓存中没有找到就继续下载过程
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
从缓存中查询图片时,最终会调用 SDImageCache 类的queryCacheOperationForKey
函数,来分别从内存和缓存中查找图片。
SDImageCache缓存类
SDImageCache 负责处理内存缓存及磁盘缓存。其中磁盘缓存的读写操作是异步的,这样就不会对UI操作造成影响。在SDImageCache内部有一个SDMemoryCache
类型的memoryCache
属性和一个SDDiskCache
类型的diskCache
属性,分别处理内存缓存和磁盘缓存。
我们直接进入queryCacheOperationForKey
函数来查看具体实现:
// 异步查询缓存并在完成时调用完成的操作。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
// key是空的则直接回调,并返回nil
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
id transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// grab the transformed disk image if transformer provided
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}
// 首先检查内存缓存
// key为图片的 URL 地址, Value则是缓存图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果内存中有图片
if (image) {
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
Class animatedImageClass = image.class;
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 如果缓存中没有,会从磁盘中查询
NSOperation *operation = [NSOperation new];
// 首先判断是否需要同步查询磁盘
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
// 从磁盘进行查询
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
// 进行回调
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
如果在缓存中没有找到图片,SDWebImageManager管理类会调用callDownloadProcessForOperation
函数来进行网络请求下载图片。
SDWebImageDownloader下载管理器
SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载的管理。在内部设置了网络请求超时时间和最大并发数:
/**
* The maximum number of concurrent downloads.
* Defaults to 6.
*/
@property (nonatomic, assign) NSInteger maxConcurrentDownloads;
/**
* The timeout value (in seconds) for each download operation.
* Defaults to 15.0.
*/
@property (nonatomic, assign) NSTimeInterval downloadTimeout;
我们来到callDownloadProcessForOperation
函数查看下载实现
:
// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Check whether we should download image from network
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];
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:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
@weakify(operation);
operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// Download operation cancelled by user before sending the request, don't block failed URL
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} 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];
}
}];
} else if (cachedImage) {
[self callCompletionBlockForOperation:operation 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
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
图片下载完成后会调用callStoreCacheProcessForOperation
函数来将图片进行缓存。
更过技术知识扫码关注微信公众号