引导
SDWebImage 是我们经常使用的一个异步图片加载库,在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。
本篇文章主要从 [SDWebImage(Bundle version 3.7.5 -> 4.3.3) 层次结构 / 场景思维 / 总结笔记
] 整理,该模块将系统化学习,后续替换、补充文章内容 ~
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出槽点,以提高文章质量@CoderLN著;
目录:
- 释义
- 层次结构
1、SDWebImageClass Architecture
2、UIView+WebCache.m
2、SDWebImageManager.m
3、SDImageCache.m
4、SDWebImageDownloader.m- 总结笔记
1、基本使用
2、原理
3、内部细节- 场景思维
1、API 接口设计
2、线程安全
3、回调设计- SourceCode 、 ToolsClass、Public-Codeidea
释义
This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like UIImageView, UIButton, MKAnnotationView.
官方释义:
这个库提供了一个支持缓存的异步图像下载器。为方便起见,我们增加了像UI元素类别 UIImageView,UIButton,MKAnnotationView。
层次结构
SDWebImageClass Architecture
这里引用官方一张图解:4.3.3
注解:
SDWebImage
其主要层次结构有,最上层(UIView (WebCache) 类别
)、逻辑层(SDWebImageManager 管理者
)、业务层(SDImageCache 缓存 + SDWebImageDownloader 下载
)组成。
通过对UIImageView
的类别扩展来实现异步加载图片的工作。主要用到的对象:
-
1、
UIImageView (WebCache)
类别,入口封装,实现读取图片完成后的回调 -
2、
SDWebImageManager
,图片的管理者,记录那些图片正在读取下载。向下层读取Cache
(调用SDImageCache
),或向网络读取对象(调用SDWebImageDownloader
)。实现SDImageCache
和SDWebImageDownloader
的回调。 -
3、
SDImageCache
,根据 URLKEY 的MD5摘要对图片进行存储和读取(内存、磁盘),及实现图片和内存清理工作。 -
4、
SDWebImageDownloader
,根据URL向网络下载数据;实现部分读取(下载选项为渐进式下载progressDownload)和全部读取后再通知回调两种方式。
.h文件
1、UIView+WebCache.m
#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Framework-Codeidea
- (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];
}
#pragma mark - ↑
#pragma mark - 最上层:UIView+WebCache; Bundle version 4.3.3
#pragma mark - 核心代码:读取下载图片 (所有外部API sd_setImageWithURL:入口方法都将会汇总到这,只是传递的参数不同而已)
/**
* @param url 图片地址链接
* @param placeholder 占位图
* @param options 下载图片的枚举。包括优先级、是否写入硬盘等
* @param operationKey 一个记录当前对象正在加载操作的key、保证只有最新的操作在进行、默认为类名。
* @param setImageBlock 给开发者自定义set图片的callback
* @param progressBlock 下载进度callback
receivedSize 已经下载的数据大小
expectedSize 要下载图片的总大小
targetURL URL地址
* @param completedBlock 下载完成的callback(sd已经给你set好了、只是会把图片给你罢了)
image 请求的 UIImage,如果出现错误image参数是nil
error 如果图片下载成功则error为nil,否则error有值
cacheType 图片缓存类型(TypeNone:网络下载、TypeDisk:使用磁盘缓存、TypeMemory:使用内存缓存)
imageURL URL地址
* @param 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 {
// 以当前实例的class作为OperationKey
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 取消当前OperationKey下正在进行的操作。
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// SD会把这个 URL 通过运行时 objc_setAssociatedObject 的方法绑定到这个 UIView 中
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 首先判断如果传入的下载选项options不是SDWebImageDelayPlaceholder 延迟显示占位图片,那么在主线程中设置占位图片。
if (!(options & SDWebImageDelayPlaceholder)) {
if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
dispatch_group_enter(group);
}
//到主线城更新UI
dispatch_main_async_safe(^{
//set 占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {//如果url不为空
// 首先先检查 activityView 是否可用,可用的话给 ImageView 正中间添加一个活动指示器并旋转,加载图片完成或失败都会清除掉
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
// 允许开发者指定一个manager来进行操作
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
__weak __typeof(self)wself = self;// 避免循环引用
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;
//移除小菊花
[self sd_removeActivityIndicator];
if (!sself) { return; }
//是否不插入图片
//如果图片下载完成,且传入的下载选项为手动设置图片则直接执行completedBlock回调,并返回
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
//如果没有得到图像
//如果传入的下载选项为延迟显示占位图片,则设置占位图片到UIImageView上面,并刷新重绘视图
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 ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
dispatch_group_enter(group);
dispatch_main_async_safe(^{
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
// ensure completion block is called after custom setImage process finish
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
callCompletedBlockClojure();
});
} else {
dispatch_main_async_safe(^{
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
callCompletedBlockClojure();
});
}
}];
// 记录当前操作:在读取图片之前,将operation存到ImageView的 SDOperationsDictionary中,为前面取消当前OperationKey下正在进行的操作存储。
// typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
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);
}
});
}
}
2、SDWebImageManager.m
#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Framework-Codeidea
#pragma mark - ↑
#pragma mark - 逻辑层:SDWebImageManager;Bundle version 4.3.3
#pragma mark - 加载图片核心方法;调度图片的下载(Downloader)和缓存(Cache),并不依托于 UIView+WebCache,完全可单独使用。
/**
* @param url 图片地址链接
* @param options 下载图片的枚举。包括优先级、是否写入硬盘等
* @param progressBlock 下载进度callback
* @param completedBlock 下载完成的callback
data 图片的二进制数据
finished
1.如果图像下载完成、或没有使用SDWebImageDownloaderProgressiveDownload 则为YES 1
2.如果使用了 SDWebImageDownloaderProgressiveDownload 渐进式下载选项,此block会被重复调用
1)下载完成前,image 参数是部分图像,finished 参数是 NO 0
2)最后一次被调用时,image 参数是完整图像,而 finished 参数是 YES
3)如果出现错误,那么finished 参数也是 YES
*
* @return SDWebImageOperation对象
*/
- (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
//没有completedblock,那么调用这个方法是毫无意义的
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是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是否是NSURL类型的,如果不是则直接设置为nil
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
//初始化一个下载操作的对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;//初始化设定该URL是正确的
if (url) {
//加互斥锁,检索请求图片的URL是否在曾下载失败的集合中(URL黑名单)
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];//线程安全s
}
}
//url为空 || (未设置失败重试 && 这个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;
}
//将操作添加到正在进行的操作数池
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//默认就是url作为key、也可以自定义mananger的相关block
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;// 防止循环引用
//查找URLKEY对应的图片缓存是否存在,查找完毕之后把该图片(存在|不存在)和该图片的缓存方法以block的方式传递
//缓存情况查找完毕之后,在block块中进行后续处理(如果该图片没有缓存·下载|如果缓存存在|如果用户设置了下载的缓存策略是刷新缓存如何处理等等)
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//如果被取消则把当前操作从runningOperations数组中移除,并直接返回
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// Check whether we should download image from network
//(图片不存在||下载策略为刷新缓存)且(shouldDownloadImageForURL不能响应||该图片存在缓存)
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
//从此处开始,一直在处理downloaderOptions(即下载策略)
//如果图像存在,但是下载策略为刷新缓存,则通知缓存图像并尝试重新下载
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
//下面是根据调用者传进来的option,来匹配设置了哪些,就给downloaderOptions赋值哪些option
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {//如果图片存在,且下载策略为刷新刷新缓存
// force progressive off if image already cached but forced refreshing
//如果图像已缓存,但需要刷新缓存,那么强制进行刷新
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
//忽略从NSURLCache读取图片
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//到此处位置,downloaderOptions(即下载策略)处理操作结束
// `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) {//如果下载失败,则处理结束的回调,在合适的情况下把对应图片的URL添加到黑名单中
[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) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];//失败记录
}
}
}
else {
if ((options & SDWebImageRetryFailed)) {//失败重新下载
@synchronized (self.failedURLs) {
[self.failedURLs removeObject: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.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
//缩放
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
//如果下载策略为SDWebImageRefreshCached且该图片缓存中存在且未下载下来,那么什么都不做
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);
}
//用户处理的后若未生成新的图片、则保存下载的二进制文件。
//不然则由imageCache内部生成二进制文件保存
[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;
}
3、SDImageCache.m
#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Framework-Codeidea
#pragma mark - ↑
#pragma mark - 业务层
#pragma mark - 缓存&&磁盘操作:SDImageCache;Bundle version 4.3.3
/*
检查要下载图片的缓存情况
1.先检查是否有内存缓存;如果有内存缓存,再从磁盘读取diskData一起回调;
2.如果没有内存缓存则检查是否有沙盒缓存
3.如果有沙盒缓存,则把该图片写入内存缓存并处理doneBlock回调
*/
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
//如果缓存对应的key为空,则直接返回,并把存储方式(无缓存)通过block块以参数的形式传递
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
// 检查该 URLKEY 对应的内存缓存,如果存在内存缓存,再从磁盘读取diskData一起回调;并把图片和存储方式(内存缓存)通过block块以参数的形式传递
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];//创建一个操作
//使用异步函数,添加任务到串行队列中(会开启一个子线程处理block块中的任务)
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {//如果当前的操作被取消,则直接返回
// do not call the completion if cancelled
return;
}
@autoreleasepool {
// 检查该KEY对应的磁盘缓存
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
//如果存在磁盘缓存,且应该把该图片保存一份到内存缓存中,则先计算该图片的cost(成本)并把该图片保存到内存缓存中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
//使用NSChache缓存。
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
//线程间通信,在主线程中回调doneBlock,并把图片和存储方式(磁盘缓存)通过block块以参数的形式传递
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
return operation;
}
4、SDWebImageDownloader.m
#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Framework-Codeidea
#pragma mark - ↑
#pragma mark - 下载操作:SDWebImageDownloader;Bundle version 4.3.3
//核心方法:下载图片的操作
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;//为了避免block的循环引用
//处理进度回调|完成回调等,如果该url在self.URLCallbacks并不存在,则调用createCallback block块
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{//创建下载operation
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;//处理下载超时,如果没有设置过则初始化为15秒
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
//创建下载策略
/*
NSURLRequestUseProtocolCachePolicy:默认的缓存策略
1)如果缓存不存在,直接从服务端获取。
2)如果缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端.
NSURLRequestReloadIgnoringLocalCacheData:忽略本地缓存数据,直接请求服务端下载。
*/
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
//创建下载请求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
//设置是否使用Cookies(采用按位与)
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//开启HTTP管道,这可以显著降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的
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) {
//设置 https 访问时身份验证使用的凭据(默认 账号密码为空的通用证书)
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;
}];
}
注解1:
SDWebImageOptions类型: 使用位移枚举,通过按位与&按位或|的组合方式传递多个值
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0, //失败后尝试重新下载
SDWebImageLowPriority = 1 << 1, //低优先级;图像下载会推迟到滚动视图停止滚动之后再继续下载。
SDWebImageCacheMemoryOnly = 1 << 2, //只使用内存缓存
SDWebImageProgressiveDownload = 1 << 3, //渐进式下载;就像浏览器中那样,下载过程中,图像会逐步显示出来。
SDWebImageRefreshCached = 1 << 4, //刷新缓存;此选项用于处理URL指向图片发生变化的情况,被刷新会调用一次 completion block,并传递最终的图像。
SDWebImageContinueInBackground = 1 << 5, //后台下载;当App进入后台后仍然会继续下载图像,如果后台任务过期,请求将会被取消。
SDWebImageHandleCookies = 1 << 6, //处理保存在NSHTTPCookieStore中的cookies
SDWebImageAllowInvalidSSLCertificates = 1 << 7, //允许不信任的 SSL 证书;可以出于测试目的使用,在正式产品中慎用
SDWebImageHighPriority = 1 << 8, //高优先级(优先下载);此标记会将它们移动到队列前端被立即加载
SDWebImageDelayPlaceholder = 1 << 9, //延迟占位图片;此标记会延迟加载占位图像,直到图像已经完成加载
SDWebImageTransformAnimatedImage = 1 << 10, //转换动画图像;通常不会在可动画的图像上调用transformDownloadedImage代理方法,因为大多数转换代码会破坏动画文件
SDWebImageAvoidAutoSetImage = 1 << 11 //手动设置图像;下载完成后手动设置图片,默认是下载完成后自动放到ImageView上
SDWebImageScaleDownLargeImages = 1 << 12, //图像解码缩小
SDWebImageQueryDataWhenInMemory = 1 << 13, //强制同步查询内存缓存数据
SDWebImageQueryDiskSync = 1 << 14, //
SDWebImageFromCacheOnly = 1 << 15, //
SDWebImageForceTransition = 1 << 16 //
};
注解2:
位移运算符语法
1、按位与"&"
只有对应的两个二进位均为1时,结果位才为1,否则为0
例如 9&5,其实就是 1001 & 0101=0001,等于1
2、按位或"|"
只要对应的两个二进位有一个为1时,结果位就为1,否则为0。
例如 9|5,其实就是 1001 | 0101=1101,等于13
3、左移"<<"
把整数a的各二进位全部左移n位,高位丢弃,低位补0。左移n位其实就是乘以2的n次方。
例如 1<<2 就是 0001左移2=0100,等于4
总结笔记
基本使用
- 下载设置图片且需要获取下载进度
/*
* 图片下载的核心方法
需求1:下载设置图片且需要获取下载进度
解决: 包含 #import "UIImageView+WebCache.h";
采用 `- sd_setImageWithURL:placeholderImage:options:progress:completed:`(异步下载并缓存)
*
* @param url URL地址
* @param placeholder 先使用占位图片,当图片加载完成后再替换.
* @param options 图片下载选项,默认SDWebImageRetryFailed失败后重新连接(参考SDWebImageOptions位移枚举);
* @param progressBlock 下载进度回调
receivedSize 已经下载的数据大小
expectedSize 要下载图片的总大小
targetURL URL地址
* @param completedBlock 操作成功回调
image 请求的 UIImage,如果出现错误image参数是nil
error 如果图片下载成功则error为nil,否则error有值
cacheType 图片缓存类型(TypeNone:网络下载、TypeDisk:使用磁盘缓存、TypeMemory:使用内存缓存)
imageURL: URL地址
*/
- (void)download11 {
[self.imageView sd_setImageWithURL:requestUrl placeholderImage:[UIImage imageNamed:@"pbw"] options:SDWebImageRetryFailed progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
NSLog(@"progress %f \n targetURL %@",1.0 * receivedSize / expectedSize,targetURL);
} completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
NSLog(@"download11-- %@",[NSThread currentThread]);// {number = 1, name = main}
self.imageView.image = image;
switch (cacheType) {
case SDImageCacheTypeNone:
NSLog(@"网络下载");
break;
case SDImageCacheTypeMemory:
NSLog(@"使用内存缓存");
break;
case SDImageCacheTypeDisk:
NSLog(@"使用磁盘缓存");
break;
default:
break;
}
}];
}
- 只需要获得一张图片
/*
* 图片下载的核心方法
需求2:只需要获得一张图片
解决: 包含 #import "SDWebImageManager.h"
采用 `- loadImageWithURL:options:progress:completed:`(异步下载并缓存)
*/
- (void)download22
{
[[SDWebImageManager sharedManager] loadImageWithURL:requestUrl options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
// 这里为什么不会调用 ?
NSLog(@"progress %f \n targetURL %@",1.0 * receivedSize / expectedSize,targetURL);
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
NSLog(@"download22-- %@",[NSThread currentThread]);// {number = 1, name = main}
NSLog(@"imageURL %@ \n finished%d",imageURL,finished);
self.imageView.image = image;
switch (cacheType) {
case SDImageCacheTypeNone:
NSLog(@"网络下载");
break;
case SDImageCacheTypeMemory:
NSLog(@"使用内存缓存");
break;
case SDImageCacheTypeDisk:
NSLog(@"使用磁盘缓存");
break;
default:
break;
}
}];
}
- 图片下载不需要任何的缓存处理
/*
* 图片下载的核心方法
需求3:图片下载不需要任何的缓存处理
解决: 包含 #import "SDWebImageDownloader.h"
采用 `- downloadImageWithURL:options:progress:completed:`(内部不做缓存处理)
*/
- (void)download33
{
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:requestUrl options:SDWebImageDownloaderProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
NSLog(@"progress %f \n targetURL %@",1.0 * receivedSize / expectedSize,targetURL);
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
NSLog(@"download33-- %@",[NSThread currentThread]);// {number = 1, name = main}
NSLog(@"finished %d",finished);
self.imageView.image = image;
}];
}
- 播放Gif图片、判断图片类型
播放Gif图片
self.imageView.image = [UIImage sd_animatedGIFWithData:imageData];
判断图片类型
NSInteger imaegFormat = [NSData sd_imageFormatForImageData:imageData];
typedef NS_ENUM(NSInteger, SDImageFormat) {
SDImageFormatUndefined = -1,
SDImageFormatJPEG = 0,
SDImageFormatPNG,
SDImageFormatGIF,
SDImageFormatTIFF,
SDImageFormatWebP,
SDImageFormatHEIC
};
原理
-
工作流程
-
最上层:UIView+WebCache下载核心方法
-
逻辑层:SDWebImageManager调度下载和缓存
-
业务层:SDImageCache缓存&Downloader磁盘操作
内部细节
- 磁盘目录位于哪里?
#pragma mark - SDImageCache.m
缓存在磁盘沙盒目录下 Library/Caches
二级目录为~/Library/Caches/default/com.hackemist.SDWebImageCache.default
//设置磁盘缓存路径
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
//获得caches路径,该框架内部对图片进行磁盘缓存,设置的缓存目录为沙盒中Library的caches目录下
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
//在caches目录下,新建一个名为【fullNamespace】的文件,沙盒缓存就保存在此处
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
//使用指定的命名空间实例化一个新的缓存存储和目录
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
//拼接默认的磁盘缓存目录
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
}
}
//你也可以通过下面方法来自定义一个路径。但这个路径不会被存储使用、是给开发者自定义预装图片的路径。
[[SDImageCache sharedImageCache] addReadOnlyCachePath:bundledPath];
- 最大并发数量、超时时长s
_downloadQueue = [NSOperationQueue new]; //创建下载队列:非主队列(在该队列中的任务在子线程中异步执行)
_downloadQueue.maxConcurrentOperationCount = 6; //设置下载队列的最大并发数:默认为6
_downloadTimeout = 15.0;
- 默认的最大缓存时间为1周
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
- 缓存文件的保存名称如何处理?
1.写入缓存时、直接用图片url作为key
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
2.写入磁盘时,对key(通常为URL)进行MD5加密,加密后的密文作为图片的名称,可以防止文件名过长。
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
NSString *filename = [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
return filename;
}
- 如何判断图片的类型?
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
//在判断图片类型的时候,只匹配NSData数据第一个字节。
// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
[data getBytes:&c length:1];//获得传入的图片二进制数据的第一个字节
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52: {
if (data.length >= 12) { //WEBP :是一种同时提供了有损压缩与无损压缩的图片文件格式
//RIFF....WEBP
//获取前12个字节
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
//如果以『RIFF』开头,且以『WEBP』结束,那么就认为该图片是Webp类型的
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
break;
}
case 0x00: {
if (data.length >= 12) {
//....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
if ([testString isEqualToString:@"ftypheic"]
|| [testString isEqualToString:@"ftypheix"]
|| [testString isEqualToString:@"ftyphevc"]
|| [testString isEqualToString:@"ftyphevx"]) {
return SDImageFormatHEIC;
}
}
break;
}
}
return SDImageFormatUndefined;
}
- 图片缓存类型?
SDImageCacheType cacheType(TypeNone:网络下载、TypeDisk:使用磁盘缓存、TypeMemory:使用内存缓存)
- 框架内部对内存警告的处理方式?
// Subscribe to app events
//监听应用程序通知
// init方法 监听到(应用程序发生内存警告)通知,调用didReceiveMemoryWarning方法,移除所有内存缓存;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//当监听到(程序将终止)调用deleteOldFiles方法,清理过期文件(默认大于一周)的磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
//当监听到(进入后台),调用backgroundDeleteOldFiles方法,清理未完成、长期运行的任务缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
- 该框架进行缓存处理的方式?
可变字典(以前) ---> (SDMemoryCache: NSCache) 缓存处理
cache.totalCostLimit = 5;// 设置最大缓存控件的总成本, 如果发现存的数据超过中成本那么会自动回收之前的对
cache.countLimit = 5;// 设置最大缓存文件的数量, 示例:多图下载综合案例
- 队列中任务的处理方式?
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //通过该属性,可以修改下载操作执行顺序
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder; //下载任务的执行方式:所有下载操作将按照队列的先进先出方式执行
- SDWebImageDownloader 如何下载图片?
发送网络请求下载图片,NSURLSession (NSURLSessionDataDelegate);
- 磁盘清理的原则?
程序将终止
首先移除早于过期日期的文件(kDefaultCacheMaxCacheAge = 1 week)。
其次如果剩余磁盘缓存空间超出最大限额,则按时间排序再次执行清理操作,循环依次删除最早的文件,直到低于期望的缓存限额的 1/2 (currentCacheSize < self.maxCacheSize / 2)。
内存警告
如果发生内存警告会收到通知,对应调用didReceiveMemoryWarning:方法,直接把把所有的内存缓存都删除。
程序进入后台
如果程序进入后台会收到通知,对应调用backgroundDeleteOldFiles方法,清理未完成、长期运行的任务task 缓存。
场景思维
API 接口设计
#import "UIImageView+WebCache.h"
#import "UIButton+WebCache.h"
#import "UIImageView+HighlightedWebCache.h"
#import "UIView+WebCache.h"
所有外层API与具体业务无关,使得SDWebImageManager可以脱离View层单独运作。
线程安全
//加互斥锁,检索请求图片的URL是否在曾下载失败的集合中(URL黑名单)
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];//线程安全s
}
if ((options & SDWebImageRetryFailed)) {//失败重新下载
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];//从失败记录移除
}
}
所有可能引起资源抢夺的对象操作、全部有条件锁保护。
回调设计
SDWebImage中使用了两种、Block以及Delegate。
-
Block
单个图片的分类、单个图片的下载。
每个操作任务中必现的progress以及completed。
所以、有很强的个体绑定需要或者使用次数不多时、倾向使用block -
Delegate
SDWebImageManager下载完成之后的自定义图片处理、是否下载某个url。
这两个方法如果需要的话都是将会调用多次的。所以、用Delegate更好、可以将方法常驻。 -
UITableView的使用Delegate、是用为在滚动途中、代理方法需要被不断的执行。
UIButton也是将会被多次点击。
UIView的动画/GCD则可以使用Block、因为只执行一次、用完释放。
所以、在日常使用中、我们也可以参考上述原则进行设计。 -
NSMapTable
用NSMapTable代替字典来存储当前正在进行的操作、并且将value设置为 NSPointerFunctionsWeakMemory。防止对应value因为强引用不能自动释放。
// typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
@implementation UIView (WebCacheOperation)
- (SDOperationsDictionary *)sd_operationDictionary {
@synchronized(self) {
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
最后分享一下学习多线程自定义NSOperation
下载图片思路:
文字解析:
-
1.根据图片的
url
先去检查images
(内存缓存)中该图片是否存在,如果存在就直接显示到cell
上;否则去检查磁盘缓存(沙盒)。 -
2.如果有磁盘缓存(沙盒),加载沙盒中对应的图片显示到
cell
上,再保存一份到内存缓存;否则先显示占位图片,再检查operations
(操作缓存)中该图片是否正在下载,如果是 就等待下载;否则创建下载operations
操作任务,保存到操作缓存中去下载。 -
3.下载完成后(需要主动刷新显示(采用局部刷新)),将操作从操作缓存中移除,将图片在内存缓存(先) 和 沙盒(后)中各保存一份。
参考:
https://www.jianshu.com/p/3b8a7ae966d3
网友评论