iOS开发历经了这么多年,说起关于图片的第三方库,相信大家首先想起的就是SDWebImage
,可以说SDWebImage
把图片加载优化到了方方面面,我们这一部分就来看看SDWebImage
的源码,来看看它究竟做了些什么。
我们首先从最常使用的API开始,在我们加载网络图片的时候都会使用<SDWebImage/UIImageView+WebCache.h>
中的这一个方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url
我们再去<SDWebImage/UIImageView+WebCache.m>
中去看一下它的源码
- (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 {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed: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];
}
而这个方法中调用了sd_internalSetImageWithURL:
,这是核心的一句代码,之前的方法都是存在于UIImageView+WebCache
文件中,而这一句核心代码的实现是在UIView+WebCache
文件中,UIImageView+WebCache
只是用于提供公共接口,而实现代码则是写在UIView+WebCache
中,这么做的原因很简单,因为SDWebImage
不仅仅可以为UIImageView
加载图片,也可以为UIButton
等视图控件加载图片,因此实现方法统一写在父类UIView
中。
我们再来看看sd_internalSetImageWithURL:
方法的内容:
- (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 {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
#endif
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
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) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];
#endif
// if the progress not been updated, mark it to complete state
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
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;
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 = sself.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
看起来很长,很庞大的一个方法,但是不要被吓到,我们将它拆分开来一点一点的分析就能理解其中的奥秘。
先来看第一步:
//获取一个操作的key,如果没有获取到就将当前调用此方法的类名作为此次操作的key
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
//根据此次操作的key取消此前对应的下载任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//使用关联对象为当前调用SDWebImage的类添加了一个属性,这个属性就是图片的地址
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
那么这个validOperationKey
是什么呢?SDWebImage
会将所有的操作记录在一个字典中,通过键值对来管理每一次操作,我们来看一下sd_cancelImageLoadOperationWithKey:
这个方法,这是要根据key
取消操作,因为如果调用方如果之前正在执行操作,避免一个调用方存在多个异步任务,因此要先cancel
掉。这个方法存在于UIView+WebCacheOperation
文件中,是专门用于管理操作的一个类,这个关联对象动态的为调用方增加了一个属性,用于记录图片的URL,以便之后获取。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
//如果存在key
if (key) {
// Cancel in progress downloader from queue
//获取保存操作的字典
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
//对关键字典进行操作时上锁,方式其他线程同时修改,很多优秀的开源库对集合类型进行操作时都会进行加锁操作。
@synchronized (self) {
//根据key找到对应的操作
operation = [operationDictionary objectForKey:key];
}
//如果找到操作
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 将当前操作取消
[operation cancel];
}
@synchronized (self) {
//在操作字典中删除对应的key
[operationDictionary removeObjectForKey:key];
}
}
}
}
我们继续看这个方法的源码第二步:
//如果没有设置默认图片延迟加载的选项,则在安全线程加载默认图片
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
第三步SDWebImageManager
的初始化,SDWebImageManager
和名字一样,它是SDWebImage
的管理者,它有四个关键属性SDImageCache,SDWebImageDownloader,failedURLs,runningOperations
分别管理图片的缓存和下载,失败的链接以及正在运行的操作。
//如果有自定义的管理者就使用自定义管理者,否则使用单例进行初始化。
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
使用manager
来进行下载操作,并用operation
接收返回值,operation
是遵守SDWebImageOperation
协议的操作, 该协议只有一个取消方法.
//使用manager来进行下载操作
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
// 是否应该调用完成回调,下载完成或者设置了避免自动设置图片选项为YES
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 是否应该不设置图片, 下载图片存在并且设置了避免自动设置图片选项 或者 下载图片不存在并且没有设置延迟加载占位图选项时 为YES;
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
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;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
// 下载好了图片并且没有设置避免自动设置图片选项
//将下载好的图片设置给目标view
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
//如果没下载成功并且设置延迟显示占位图则直接将占位图赋给目标view
targetImage = placeholder;
targetData = nil;
}
dispatch_main_async_safe(^{
//调用设置图片方法,将图片赋值给对应的targetView
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
callCompletedBlockClojure();
});
}];
//将操作与key绑定起来
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上面是简化后的具体方法,最主要的作用就是根据用户所选择的options
来决定图片的显示方式,设置临时占位图,将操作以及其对应的key绑定放入操作字典中。
SDWebImageManager
现在我们就来看一下SDWebImageManager
是如何进行图片的加载操作的,看一下它的核心方法loadImageWithURL
:
- (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.
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;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
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;
}
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
__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;
}
// Check whether we should download image from network
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];
}
// 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
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
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
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);
}
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else {
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
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
} 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;
// 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);
}
[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];
}
}];
return operation;
}
依旧很长的一个方法,没关系,还是分解开来看。先看第一部分,这一部分是用于校验我们图片下载的URL
是否有效。
// 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是不是字符串类型,如果是字符串类型则强制转为URL类型。
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
//如果url不是NSURL类型,则将它赋值为nil,方式崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
//这里声明了一个叫做“组合操作”的operation,它的属性有我们的SDWebImageManager,还有一个cancel属性,一个存取缓存的线程属性,一个与下载有关的Token属性,后面会讲到,以及一个canel方法,用于终止操作。
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
//将是否是失败过的Url选项设置为NO
BOOL isFailedUrl = NO;
if (url) {
//操作关键数组上锁
LOCK(self.failedURLsLock);
//如果这个url之前失败过则将选项设置为YES
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果url的绝对字符串长度为0,并且没有设置失败重新下载,并且这个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;
}
当URL
经过一系列判断证明了自己是个可用的URL
,接下来就开始进行了下载操作。
//之前说过我们的manager有一个数组专门存储当前正在进行中的操作,url判断没问题,那么操作就要开始了,把当前的操作加入到这个数组中。
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
//先通过url当做key,去缓存字典中看看是不是之前有下载过并且留下了缓存。
NSString *key = [self cacheKeyForURL:url];
//根据缓存选项为cacheOptions赋值
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
//防止循环引用使用weak修饰
__weak SDWebImageCombinedOperation *weakOperation = operation;
//这里大家注意,manager并不会直接去下载,而且会开启一个子线程,通过子线程去缓存中查找是否有图片的缓存。
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
//这里开始判断是不是要进行下载
//通过是否有缓存,是否选择了刷新缓存等选项来判断是否要进行下载
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
// 如果在缓存中找到图片,但设置了SDWebImageRefreshCached选项,会通知缓存的图片,并尝试重新下载,以便让NSURLCache从服务器刷新它
// 回调缓存图片然后重新下载
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];
}
// 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;
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
//此处避免循环引用设置weak修饰符
__weak typeof(strongOperation) weakSubOperation = strongOperation;
//*************从这里开始正式使用下载器开始下载
//downloadToken用于标记下载任务便于管理,使用图片url以及下载选项进行图片的下载。
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];
//因为下载失败这里来判断是否要记录这个失败的URL
BOOL shouldBlockFailedURL;
// Check whether we should block failed 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) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
//**********这里开始是下载成功的操作
else {
//如果之前下载失败过,但这次下载成功了,那么就把URL从失败数组中移除
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
//判断是不是要进行磁盘缓存
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.
//如果使用了自定义manager,则会进行图片的处理,因为默认的manager在下载器中会处理图片,避免再次进行图片处理
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:)]) {
//使用GCD来对图片进行处理
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;
// pass nil if the image was transformed, so we can recalculate the data from the image
// 缓存存储被处理的图片,如果已经处理了图片,则imageData设为nil,因此可以重新计算图片数据
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) {
//使用GCD对图片进行缓存
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];
}
}];
return operation;
总结一下这一段的操作:
1.判断
URL
是不是可以使用,可以使用将operation
加入正在运行的操作数组中。
2.operation
开启子线程去查询缓存,使用URL作为key查找缓存,判断是否查到缓存,有一些情况即使找到缓存也要继续下载,再判断是否要下载。
3.确定要下载,使用Downloader
进行图片的下载,下载失败将URL
加入到失败URL
的数组中,下载成功进行下一步判断。
4.是否需要处理图片,如果需要处理图片,那么就在多线程GCD
中处理图片处理完成缓存处理过的图片,如果不需要处理图片,直接缓存图片,完成操作将operation
从操作数组中移除。
5.如果无需下载,那么直接从缓存中拿出图片缓存返回给UIImageView
去使用。
使用到的技术细节:
1.对关键的集合类操作使用上锁解锁操作,防止其他线程侵入修改内容。
2.使用统一的操作数组管理正在进行的操作,以及使用failURLs
数组记录曾经失败过的URL
。
3.耗时的缓存处理操作使用NSOperation
技术在子线程中进行,处理图片的耗时操作在GCD
中完成。
4.SDWebImage
在每次图片请求时的判断都十分的严谨,是否要下载,还是直接取缓存图片,以及根据条件更新缓存。
5.在缓存时会判断是否要缓存在内存中,如果不是则缓存到磁盘中。
SDImageCache获取缓存
现在我们来看看SDWebImage
的缓存操作,在加载图片的方法中,首先会使用queryCacheOperationForKey:
去查询缓存,看一下这个方法的源码。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
//没有缓存key,直接调用完成block,返回nil
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
//首先在内存缓存中查找图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
//判断是否仅在内存缓存中查询图片,如果存在图片并且没有设置强制取磁盘图片,则判断仅仅在内存查找
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//建立一个子线程
NSOperation *operation = [NSOperation new];
//在串行io队列中异步执行提取磁盘缓存操作
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
//在自动释放池中处理磁盘数据,防止临时对象太多造成内存溢出
@autoreleasepool {
//去磁盘中查找数据
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
//在内存中找到图片
if (image) {
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// decode image data only if in-memory cache missed
//在内存缓存中没找到图片,则去磁盘中读取数据编码图片
diskImage = [self diskImageForKey:key data:diskData options:options];
//如果找到了磁盘数据并且设置在内存缓存中进行保存
if (diskImage && self.config.shouldCacheImagesInMemory) {
//将磁盘取出来的图片数据存储在内存缓存中。
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
//如果存在完成block
if (doneBlock) {
//SDWebImage对内存缓存是同步的,磁盘缓存是异步的,如果强制磁盘同步存取则会同步存取
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;
}
我们来看看它获取缓存的步骤:
1.判断是否存在缓存
key
,没有直接执行完成block,返回nil。
2.存在key则先去内存缓存中查找图片,判断是不是仅仅在内存中查找就可以,如果在内存缓存中找到图片,执行完成block将内存缓存的图片返回。
3.如果内存缓存没有找到,或者用户设置同时要去磁盘中寻找,那么我们可以选择是异步去执行磁盘操作还是同步执行queryDiskBlock
。
4.如果在磁盘中找到了图片,则将磁盘中找到的数据提取出来存储在内存缓存中。
再来看看这里面涉及到的一些知识点:
1.内存缓存的获取
SDWebImage
会默认同步进行,因为耗时短,但是磁盘的耗时操作就会选择使用GCD
的异步执行。
2.将涉及大量编解码的操作放在自动释放池中执行,防止大量临时对象的产生造成内存溢出。
3.如果在磁盘中找到了缓存数据,也会将其取出存在内存缓存中方便提取操作。
SDImageCache进行缓存
再来看看图片下载完成的缓存操作:
- (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)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
//将图片编码成相应的格式
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
//将图片存储到磁盘中
[self _storeImageDataToDisk:data forKey:key];
}
//执行完成block
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
这里要注意,将图片编码成相应的格式PNG
或者是JPEG
然后再进行磁盘数据的缓存。
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;
}
// 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 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;
}
//模拟后进先出
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;
}];
}
注意这里为什么要后进先出,假如我们是一个滑动界面,当我们在第一页还未下载成功时迅速滑动到下一页,如果是常规的FIFO
那么就会在我们观看第二页时依旧在执行上一页的图片下载操作。
下载操作执行addProgressCallback:
的实现代码:
- (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.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
LOCK(self.operationsLock);
//根据URL获取一个下载操作
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
if (!operation || operation.isFinished) {
//操作若不存在则创建一个下载操作
operation = createCallback();
__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);
};
//将本次下载操作加入记录操作的字典中。
[self.URLOperations setObject:operation forKey:url];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
//将本次操作加入线程池中
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
// 保存progressBlock和completedBlock
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
SDWebImageDownloader
负责对下载操作进行设置,而真正的下载操作则是由SDWebImageDownloaderOperation
来执行,整个的下载流程都是使用NSOperation
来进行异步下载,下载完成后图片会根据用户的选项存入内存缓存亦或是磁盘缓存。
SDWebImageCodersManager
我们为UIImageView
设置图片时,无论SDWebImage
是从磁盘获取缓存,还是从网络下载图片都会调用SDWebImageCodersManager
中的一个方法decompressedImageWithImage:
这个方法就是我上一篇笔记中提到的,异步将图片进行解压缩然后进行渲染。
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
//自动释放掉位图掉上下文以及所有的实例帮助系统释放内存控件
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
// device color space
//设置颜色空间为RGB
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
//判断图片是否支持alpha通道
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
//设置字节顺序为32位
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
//当图片不包含 alpha的时候使用kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
//位图的宽度和高度,分别赋值为图片的像素宽度和像素高度
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
//创建位图上下文
//第一个参数data, 设置NULL ,那么系统就会为我们自动分配和释放所需的内存
//后两个填位图的位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可
//kBitsPerComponent为8,在像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8
//位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
//设置颜色空间为RGB
//设置位图布局信息。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
//根据我们的参数设置绘制图片,将其解压,达到子线程强制解压缩,避免主线程解压造成性能问题。
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
//记得释放CG类型数据,CG类型数据的内存管理依旧需要我们手动管理
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
总结
仅仅分析了部分功能,SDWebImage
就使用到了相当多的iOS知识点:
1.对集合类的使用非常多,失败
URL
的数组,记录操作的字典,正在运行线程的数组等等,而且对集合类的管理及其细致,无论加入还是移除的判断都考虑的面面俱到。
2.对于锁的使用,由于使用了很多的多线程技术,因此在操作集合类的时候都会选择上锁,防止其他线程同时进行修改,使用到了@synchronized
以及GCD
的dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER)
来对集合类进行锁的操作。
3.多线程的使用,无论是下载还是缓存操作都使用到多线程,下载使用了NSOperation
而缓存使用的则是GCD的dispatch_async
。
4.内存管理的应用,在ARC时代很少会需要手动键入内存管理语句,而SDWebImage
则在可能产生大量临时变量的时候使用自动释放池,并且手动管理CG类型
。
5.优化图片渲染的流程,异步强行解压缩位图,避免主线程解压影响性能。
6.运行时的应用,使用关联对象为类增加属性。
7.运用了多种设计模式。
网友评论