本文基于 SDWebImage 5.6。重读的缘由也是因为发现它的 API 在不断迭代,许多结构已经不一样与早期版本,同时也是为了作一个记录。阅读顺序也会依据 API 执行顺序进行,不会太拘泥于细节,更可能是了解整个框架是如何运行的。c++
若是你们有兴趣的,强烈推荐观看官方的推荐的迁移文档,提到了5.x 版本的须要新特性,里面详细介绍其新特性和变化动机,主要 features:git
FLAnimatedImageView
);能够说,5.x 的变化在于将整个 SDWebImage 中的核心类进行了协议化,同时将图片的请求、加载、解码、缓存等操做尽量的进行了插件化处理,达到方便扩展、可替换。github
协议化的类型不少,这里仅列出一小部分:objective-c
4.4 | 5.x |
---|---|
SDWebImageCacheSerializerBlock | id<SDWebImageCacheSerializer> |
SDWebImageCacheKeyFilterBlock | id<SDWebImageCacheKeyFilter> |
SDWebImageDownloader | id<SDImageLoader> |
SDImageCache | id<SDImageCache> |
SDWebImageDownloaderProgressBlock | id<SDWebImageIndicator> |
FLAnimatedImageView | id<SDAnimatedImage> |
做为上层 API 调用是经过在 UIView + WebCache
之上提供便利方法实现的,包含如下几个 :数据库
开始前,先来看看 SDWebImageCompat.h 它定义了SD_MAC、SD_UIKIT、SD_WATCH 这三个宏用来区分不一样系统的 API 来知足条件编译,同时还利用其来抹除 API 在不一样平台的差别,好比利用 #define UIImage NSImage
将 mac 上的 NSImage 统一为 UIImage。另外值得注意的一点就是:设计模式
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
复制代码
区别于早起版本的实现:缓存
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
复制代码
#ifndef
提升了代码的严谨度,防止重复定义 dispatch_main_async_safe
关于第二点,有一篇 SD 的讨论,以及另外一篇说明 GCD's Main Queue vs. Main Thread安全
Calling an API from a non-main queue that is executing on the main thread will lead to issues if the library (like VektorKit) relies on checking for execution on the main queue.markdown
区别就是从判断是否在主线程执行改成是否在主队列上调度。由于 在主队列中的任务,必定会放到主线程执行。session
相比 UIImageView 的分类,UIButton 须要存储不一样 UIControlState
和 backgrounImage 下的 image,Associate 了一个内部字典 (NSMutableDictionary<NSString *, NSURL *> *)sd_imageURLStorage
来保存图片。
全部 View Category 的 setImageUrl:
最终收口到下面这个方法:
- (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;
复制代码
这个方法实现很长,简单说明流程:
SDWebImageContext
复制并转换为 immutable,获取其中的 validOperationKey
值做为校验 id,默认值为当前 view 的类名;sd_cancelImageLoadOperationWithKey
取消上一次任务,保证没有当前正在进行的异步下载操做, 不会与即将进行的操做发生冲突;SDWebImageManager
、SDImageLoaderProgressBlock
, 重置 NSProgress
、SDWebImageIndicator
;loadImageWithURL:
并将返回的 SDWebImageOperation
存入 sd_operationDictionary
,key 为 validOperationKey
;sd_setImage:
同时为新的 image 添加 Transition 过渡动画;稍微说明的是 SDWebImageOperation
它是一个 **strong - weak **的 NSMapTable,也是经过关联值添加的:
// key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
复制代码
用 weak 是由于 operation 实例是保存在 SDWebImageManager 的 runningOperations,这里只是保存了引用,以方便 cancel 。
A SDWebImageContext object which hold the original context options from top-level API.
image context 贯穿图片处理的整个流程,它将数据逐级带入各个处理任务中,存在两种类型的 ImageContext:
typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM; typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext; typedef NSMutableDictionary<SDWebImageContextOption, id>SDWebImageMutableContext; 复制代码
SDWebImageContextOption 是一个可扩展的 String 枚举,目前有 15 种类型。基本上,你只需看名字也能猜出个大概,文档,简单作了以下分类:
从其参与度来看,可见其重要性。
Prefetcher 它与 SD 整个处理流关系不大,主要用 imageManger 进行图片批量下载,核心方法以下:
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
复制代码
它将下载的 URLs 做为 事务
存入 SDWebImagePrefetchToken
中,避免以前版本在每次 prefetchURLs:
时将上一次的 fetching 操做 cancel 的问题。
每一个下载任务都是在 autoreleasesepool 环境下,且会用 SDAsyncBlockOperation
来包装真正的下载任务,来达到任务的可取消操做:
@autoreleasepool {
@weakify(self);
SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
@strongify(self);
if (!self || asyncOperation.isCancelled) {
return;
}
/// load Image ...
}];
@synchronized (token) {
[token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
}
[self.prefetchQueue addOperation:prefetchOperation];
}
复制代码
最后将任务存入 prefetchQueue,其最大限制下载数默认为 3 。而 URLs 下载的真正任务是放在 token.loadOperations
:
NSPointerArray *operations = token.loadOperations;
id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options context:self.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
/// progress handler
}];
NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
@synchronized (token) {
[operations addPointer:(__bridge void *)operation];
}
复制代码
loadOperations
与 prefetchOperations
均使用 NSPointerArray ,这里用到了其 NSPointerFunctionsWeakMemory
特性以及能够存储 Null
值,尽管其性能并非很好,参见:基础集合类
另一个值得注意的是 PrefetchToken 对下载状态的线程安全管理,使用了 c++11 memory_order_relaxed 。
atomic_ulong _skippedCount; atomic_ulong _finishedCount; atomic_flag _isAllFinished; unsigned long _totalCount; 复制代码
即经过内存顺序和原子操做作到无锁并发,从而提升效率。具体原理感兴趣的同窗能够自行查阅资料。
SDWebImageDownloader 是 <SDImageLoader> 协议在 SD 内部的默认实现。它提供了 HTTP/HTTPS/FTP 或者 local URL 的 NSURLSession 来源的图片获取能力。同时它最大程度的开放整个下载过程的的可配置性。主要 properties :
@interface SDWebImageDownloader : NSObject
@property (nonatomic, copy, readonly, nonnull) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderDecryptor> decryptor;
/* ... */
-(nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
@end
复制代码
其中 downloaderConfig 是支持 NSCopy 协议的,提供的主要配置以下:
/// Defaults to 6.
@property (nonatomic, assign) NSInteger maxConcurrentDownloads;
/// Defaults to 15.0s.
@property (nonatomic, assign) NSTimeInterval downloadTimeout;
/// custom session configuration,不支持在使用过程当中动态替换类型;
@property (nonatomic, strong, nullable) NSURLSessionConfiguration *sessionConfiguration;
/// 动态扩展类,须要遵循 `NSOperation<SDWebImageDownloaderOperation>` 以实现 SDImageLoader 定制
@property (nonatomic, assign, nullable) Class operationClass;
/// 图片下载顺序,默认 FIFO
@property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder;
复制代码
request modifier,提供在下载前修改 request,
/// Modify the original URL request and return a new one instead. You can modify the HTTP header, cachePolicy, etc for this URL.
@protocol SDWebImageDownloaderRequestModifier <NSObject>
- (nullable NSURLRequest *)modifiedRequestWithRequest:(nonnull NSURLRequest *)request;
@end
复制代码
一样,response modifier 则提供对返回值的修改,
/// Modify the original URL response and return a new response. You can use this to check MIME-Type, mock server response, etc.
@protocol SDWebImageDownloaderResponseModifier <NSObject>
- (nullable NSURLResponse *)modifiedResponseWithResponse:(nonnull NSURLResponse *)response;
@end
复制代码
最后一个 decryptor 用于图片解密,默认提供了对 imageData 的 base64 转换,
/// Decrypt the original download data and return a new data. You can use this to decrypt the data using your perfereed algorithm.
@protocol SDWebImageDownloaderDecryptor <NSObject>
- (nullable NSData *)decryptedDataWithData:(nonnull NSData *)data response:(nullable NSURLResponse *)response;
@end
复制代码
经过这个协议化后的对象来处理数据,能够说是利用了设计模式中的 策略模式 或者 依赖注入。经过配置的方式获取到协议对象,调用方仅需关心协议对象提供的方法,无需在乎其内部实现,达到解耦的目的。
###DownloadImageWithURL
下载前先检查 URL 是否存在,没有则直接抛错返回。取到 URL 后尝试复用以前生成的 operation:
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
复制代码
若是 operation 存在,调用
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
复制代码
并设置 queuePriority。这里用了 @synchronized(operation) ,同时 Operation 内部则会用 @synchronized(self),以保证两个不一样类间 operation 的线程安全,由于 operation 有可能被传递到解码或代理的队列中。这里 addHandlersForProgress:
会将 progressBlock 与 completedBlock 一块儿存入 NSMutableDictionary<NSString *, id> SDCallbacksDictionary
而后返回保存在 downloadOperationCancelToken 中。
另外,Operation 在 addHandlersForProgress:
时并不会清除以前存储的 callbacks 是增量保存的,也就是说屡次调用的 callBack 在完成后都会被依次执行。
若是 operation 不存在、任务被取消、任务已完成,调用 createDownloaderOperationWithUrl:options:context:
建立出新的 operation 并存储在 URLOperations 中 。同时会配置 completionBlock,使得任务完成后能够及时清理 URLOperations。保存 progressBlock 和 completedBlock;提交 operation 到 downloadQueue。
最终 operation、url、request、downloadOperationCancelToken 一块儿被打包进 SDWebImageDownloadToken, 下载方法结束。
###CreateDownloaderOperation
下载结束,咱们来聊聊 operation 是如何建立的。首先是生成 URLRequest:
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
SD_LOCK(self.HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(self.HTTPHeadersLock);
复制代码
主要经过 SDWebImageDownloaderOptions 获取参数来配置, timeout 是由 downloader 的 config.downloadTimeout 决定,默认为 15s。而后从 imageContext 中取出 id<SDWebImageDownloaderRequestModifier> requestModifier
对 request 进行改造。
// Request Modifier
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}
复制代码
值得注意的是 requestModifier 的获取是有优先级的,经过 imageContext 获得的优先级高于 downloader 所拥有的。经过这种方既知足了接口调用方可控,又能支持全局配置,可谓老小皆宜。同理,id<SDWebImageDownloaderResponseModifier> responseModifier
、id<SDWebImageDownloaderDecryptor> decryptor
也是如此。
以后会将确认过的 responseModifier 和 decryptor 再次保存到 imageContext 中为以后使用。
最后,从 downloaderConfig 中取出 operationClass 建立 operation:
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
// Custom operation class
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
复制代码
设置其 credential、minimumProgressInterval、queuePriority、pendingOperation。
默认状况下,每一个任务是按照 FIFO 顺序添加到 downloadQueue 中,若是用户设置的是 LIFO 时,添加进队列前会修改队列中现有任务的优先级来达到效果:
if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
// This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
// Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
for (NSOperation *pendingOperation in self.downloadQueue.operations) {
[pendingOperation addDependency:operation];
}
}
复制代码
经过遍历队列,将新任务修改成当前队列中全部任务的依赖以反转优先级。
SDWebImageDownloaderOperation 也是协议化后的类型,协议自己遵循 NSURLSessionTaskDelegate, NSURLSessionDataDelegate,它是真正处理 URL 请求数据的类,支持后台下载,支持对 responseData 修改(by responseModifier),支持对 download ImageData 进行解密 (by decryptor)。其主要内部 properties 以下:
@property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
@property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
@property (strong, nonatomic, nullable) NSMutableData *imageData;
@property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse`
@property (assign, nonatomic) NSUInteger expectedSize; // may be 0
@property (assign, nonatomic) NSUInteger receivedSize;
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier; // modifiy original URLResponse
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor; // decrypt image data
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding
#if SD_UIKIT
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context;
复制代码
初始化没有什么特别的,须要注意的是这里传入的 nullable session
是以 unownedSessin 保存,区别于内部默认生成的 ownedSession。若是初始化时 session 为空,会在 start
时建立 ownedSession。
那么问题来了,因为咱们需观察 session 的各个状态,须要设置 delegate 来完成,
[NSURLSession sessionWithConfiguration:delegate:delegateQueue:];
复制代码
ownedSession 的 delegate 毋庸置疑就在 operation 内部,而初始化传入 session 的 delegate 则是 downloader 。它会经过 taskID 取出 operation 调用对应实现来完成回调的统一处理和转发,例如:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
[dataOperation URLSession:session task:task didCompleteWithError:error];
}
}
复制代码
接着做为真正的消费者 operation 开始下载任务,整个下载过程包括开始、结束、取消都会发送对应通知。
在 didReceiveResponse 时,会保存 response.expectedContentLength 做为 expectedSize。而后调用 modifiedResponseWithResponse:
保存编辑后的 reponse。
每次 didReceiveData 会将 data 追加到 imageData:[self.imageData appendData:data]
,更新 receivedSizeself.receivedSize = self.imageData.length
。最终,当 receivedSize > expectedSize 断定下载完成,执行后续处理。若是你支持了 SDWebImageDownloaderProgressiveLoad
,每当收到数据时,将会进入 coderQueue 进行边下载边解码:
// progressive decode the image in coder queue
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
if (image) {
// We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
复制代码
不然,会在 didCompleteWithError 时完成解码操做:SDImageLoaderDecodeImageData
,不过在解码前须要先解密:
if (imageData && self.decryptor) {
imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}
复制代码
3. 处理 complete 回调;
关于 decode 的逻辑咱们最后聊。
基本上 Cache 相关类的设计思路与 ImageLoader 一致,会有一份 SDImageCacheConfig 以配置缓存的过时时间,容量大小,读写权限,以及动态可扩展的 MemoryCache/DiskCache。
SDImageCacheConfig 主要属性以下:
@property (assign, nonatomic) BOOL shouldDisableiCloud;
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
@property (assign, nonatomic) NSTimeInterval maxDiskAge;
@property (assign, nonatomic) NSUInteger maxDiskSize;
@property (assign, nonatomic) NSUInteger maxMemoryCost;
@property (assign, nonatomic) NSUInteger maxMemoryCount;
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
/// Defaults to built-in `SDMemoryCache` class.
@property (assign, nonatomic, nonnull) Class memoryCacheClass;
/// Defaults to built-in `SDDiskCache` class.
@property (assign ,nonatomic, nonnull) Class diskCacheClass;
复制代码
MemoryCache、DiskCache 的实例化都须要 SDImageCacheConfig 的传入:
/// SDMemoryCache
- (nonnull instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
/// SDDiskCache
- (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config;
复制代码
做为缓存协议,他们的接口声明基本一致,都是对数据的 CURD,区别在于 MemoryCache Protocl 操做的是 id 类型 (NSCache API 限制),DiskCache 则是对 NSData。
咱们来看看他们的默认实现吧。
/**
A memory cache which auto purge the cache on memory warning and support weak cache.
*/
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType> <SDMemoryCache>
@property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config;
@end
复制代码
内部就是将 NSCache 扩展为了 SDMemoryCache 协议,并加入了 *NSMapTable<KeyType, ObjectType> weakCache ,并为其添加了信号量锁来保证线程安全。这里的 weak-cache 是仅在 iOS/tvOS 平台添加的特性,由于在 macOS 上尽管收到系统内存警告,NSCache 也不会清理对应的缓存。weakCache 使用的是 strong-weak 引用不会有有额外的内存开销且不影响对象的生命周期。
weakCache 的做用在于恢复缓存,它经过 CacheConfig 的 shouldUseWeakMemoryCache 开关以控制,详细说明能够查看 CacheConfig.h。先看看其如何实现的:
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(self.weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
复制代码
因为 NSCache 遵循 NSDiscardableContent
策略来存储临时对象的,当内存紧张时,缓存对象有可能被系统清理掉。此时,若是应用访问 MemoryCache 时,缓存一旦未命中,则会转入 diskCache 的查询操做,可能致使 image 闪烁现象。而当开启 shouldUseWeakMemoryCache 时,由于 weakCache 保存着对象的弱引用 (在对象 被 NSCache 被清理且没有被释放的状况下),咱们可经过 weakCache 取到缓存,将其塞会 NSCache 中。从而减小磁盘 I/O。
这个更简单,内部使用 NSFileManager 管理图片数据读写, 调用 SDDiskCacheFileNameForKey 将 key MD5 处理后做为 fileName,存放在 diskCachePath 目录下。另外就是过时缓存的清理:
NSDirectoryEnumerator *fileEnumerator
,开始过滤;[self.fileManager removeItemAtURL:fileURL error:nil];
另一点就是 SDDiskCache 同 YYKVStorage 同样一样支持为 UIImage 添加 extendData 用以存储额外信息,例如,图片的缩放比例, URL rich link, 时间等其余数据。
不过 YYKVStorage 自己是用数据库中 manifest 表的 extended_data 字段来存储的。SDDiskCache 就另辟蹊径解决了。利用系统 API <sys/xattr.h> 的 setxattr、getxattr、listxattr 将 extendData 保存。能够说又涨姿式了。顺便说一下,它对应的 key 是用 SDDiskCacheExtendedAttributeName。
也是协议化后的类,负责调度 SDMemoryCache、SDDiskCache,其 Properties 以下:
@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
复制代码
说明:memoryCache 和 diskCache 实例是依据 CacheConfig 中定义的 class 来生成的,默认为 SDMemoryCache 和 SDDiskCache。
咱们看看其核心方法:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
复制代码
确保 image 和 key 存在;
当 shouldCacheImagesInMemory 为 YES,则会调用 [self.memoryCache setObject:image forKey:key cost:cost]
进行 memoryCache 写入;
进行 diskCache 写入,操做逻辑放入 ioQueue 和 autoreleasepool 中。
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = ... // 根据 SDImageFormat 对 image 进行编码获取
/// data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
[self _storeImageDataToDisk:data forKey:key];
if (image) {
// Check extended data
id extendedObject = image.sd_extendedObject;
// ... get extended data
[self.diskCache setExtendedData:extendedData forKey:key];
}
}
// call completionBlock in main queue
});
复制代码
另外一个重要的方法就是 image query,定义在 SDImageCache 协议中:
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass;
return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}
复制代码
它只作了一件事情,将 SDWebImageOptions 转换为 SDImageCacheOptions,而后调用 queryCacheOperationForKey:
,其内部逻辑以下:
首先,若是 query key 存在,会从 imageContext 中获取 transformer,对 query key 进行转换:
key = SDTransformedKeyForKey(key, transformerKey);
复制代码
尝试从 memory cache 获取 image,若是存在:
知足 SDImageCacheDecodeFirstFrameOnly 且遵循 SDAnimatedImage 协议,则会取出 CGImage 进行转换
// 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
}
复制代码
知足 SDImageCacheMatchAnimatedImageClass ,则会强制检查 image 类型是否匹配,不然将数据至 nil:
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
复制代码
当能够从 memory cache 获取到 image 且为 SDImageCacheQueryMemoryData,直接完成返回,不然继续;
开始 diskCache 读取,依据读取条件断定 I/O 操做是否为同步。
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
复制代码
整个 diskQuery 存在 queryDiskBlock 中并用 autorelease 包裹:
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// call doneBlock & 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];
}
}
// call doneBlock
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
}
复制代码
对于大量临时内存操做 SD 都会将其放入 autoreleasepool 以保证内存能及时被释放。
特别强调,代码若是执行到这,就必定会有磁盘读取到操做,所以,若是不是非要获取 imageData 能够经过 SDImageCacheQueryMemoryData 来提升查询效率;
最后,SDTransformedKeyForKey
的转换逻辑是以 SDImageTransformer 的 transformerKey 按顺序依次拼接在 image key 后面。例如:
'image.png' |> flip(YES,NO) |> rotate(pi/4,YES) =>
'image-SDImageFlippingTransformer(1,0)-SDImageRotationTransformer(0.78539816339,1).png'
复制代码
SDImageManger 做为整个库的调度中心,上述各类逻辑的集大成者,它把各个组建串联,从视图 > 下载 > 解码器 > 缓存。而它暴露的核心方法就一个,就是 loadImage:
@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;
@property (strong, nonatomic, nullable) id<SDImageTransformer> transformer;
@property (nonatomic, strong, nullable) id<SDWebImageCacheKeyFilter> cacheKeyFilter;
@property (nonatomic, strong, nullable) id<SDWebImageCacheSerializer> cacheSerializer;
@property (nonatomic, strong, nullable) id<SDWebImageOptionsProcessor> optionsProcessor;
@property (nonatomic, class, nullable) id<SDImageCache> defaultImageCache;
@property (nonatomic, class, nullable) id<SDImageLoader> defaultImageLoader;
- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;
复制代码
这里先简单说一下 cacheKeyFilter、cacheSerializer 和 optionsProcessor 这三个 API,其他的上面都提到过了。
SDWebImageCacheKeyFilter
默认状况下,是把 URL.absoluteString 做为 cacheKey ,而若是设置了 fileter 则会对经过 cacheKeyForURL:
对 cacheKey 拦截并进行修改;
SDWebImageCacheSerializer
默认状况下,ImageCache 会直接将 downloadData 进行缓存,而当咱们使用其余图片格式进行传输时,例如 WEBP 格式的,那么磁盘中的存储则会按 WEBP 格式来。这会产生一个问题,每次当咱们须要从磁盘读取 image 时都须要进行重复的解码操做。而经过 CacheSerializer 能够直接将 downloadData 转换为 JPEG/PNG 的格式的 NSData 缓存,从而提升访问效率。
SDWebImageOptionsProcessor
用于控制全局的 SDWebImageOptions 和 SDWebImageContext 中的参数。示例以下:
SDWebImageManager.sharedManager.optionsProcessor = [SDWebImageOptionsProcessor optionsProcessorWithBlock:^SDWebImageOptionsResult * _Nullable(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
// Only do animation on `SDAnimatedImageView`
if (!context[SDWebImageContextAnimatedImageClass]) {
options |= SDWebImageDecodeFirstFrameOnly;
}
// Do not force decode for png url
if ([url.lastPathComponent isEqualToString:@"png"]) {
options |= SDWebImageAvoidDecodeImage;
}
// Always use screen scale factor
SDWebImageMutableContext *mutableContext = [NSDictionary dictionaryWithDictionary:context];
mutableContext[SDWebImageContextImageScaleFactor] = @(UIScreen.mainScreen.scale);
context = [mutableContext copy];
return [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
}];
复制代码
接口的的第一个参数 url 做为整个框架的链接核心,却设计成 nullable 应该彻底是方便调用方而设计的。内部经过对 url 的 nil 判断以及对 NSString 类型的兼容 (强制转成 NSURL) 以保证后续的流程,不然结束调用。下载开始后又拆分红了一下 6 个方法:
分别是:缓存查询、下载、存储、转换、执行回调、清理回调。你能够发现每一个方法都是针对 operation 的操做,operation 在 loadImage 时会准备好,而后开始缓存查询。
SDWebImageCombinedOperation *operation = [SDWebImagCombinedOperation new];
operation.manager = self;
/// 1
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
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);
// 2. Preprocess the options and context arg to decide the final the result for manager
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
复制代码
loadImage 方法自己不复杂,核心是生成 operation 而后转入缓存查询。
在 operation 初始化后会检查 failedURLs 是否包含当前 url:
runningOperations
中。并将 options 和 imageContext 封入 SDWebImageOptionsResult。同时,会更新一波 imageContext,主要先将 transformer、cacheKeyFilter、cacheSerializer 存入 imageContext 作为全局默认设置,再调用 optionsProcessor 来提供用户的自定义 options 再次加工 imageContext 。这个套路你们应该有印象吧,前面的 ImageLoader 中的 requestModifer 的优先级逻辑与此相似,不过实现方式有些差别。最后转入 CacheProcess。
loadImage 过程是使用了 combineOperation,它是 combine 了 cache 和 loader 的操做任务,使其能够一步到位清理缓存查询和下载任务的做用。其声明以下:
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
/// imageCache queryImageForKey: 的 operation
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> cacheOperation;
/// imageLoader requestImageWithURL: 的 operation
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> loaderOperation;
/// Cancel the current operation, including cache and loader process
- (void)cancel;
@end
复制代码
其提供的 cancel 方法会逐步检查两种类型 opration 而后逐一执行 cancel 操做。
####CallCacheProcessForOperation
先检查 SDWebImageFromLoaderOnly 值,判断是否为直接下载的任务,
是,则转到 downloadProcess。
否,则经过 imageCache 建立查询任务并将其保存到 combineOperation 的 cacheOperation :
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
if (!operation || operation.isCancelled) {
/// 1
}
/// 2
}];
复制代码
对缓存查询的结果有两种状况须要处理:
####CallDownloadProcessForOperation
下载的实现比较复杂,首先须要决定是否须要新建下载任务,由三个变量控制:
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [self.imageLoader canRequestImageForURL:url];
复制代码
若是 shouldDownload 为 NO,则结束下载并调用 callCompletionBlockForOperation 与 safelyRemoveOperationFromRunning。此时若是存在 cacheImage 则会随 completionBlock 一块儿返回。
若是 shouldDownload 为 YES,新建下载任务并将其保存在 combineOperation 的 loaderOperation。在新建任务前,若有取到 cacheImage 且 SDWebImageRefreshCached 为 YES,会将其存入 imageContext (没有则建立 imageContext)。
下载结束后回到 callBack,这里会先处理几种状况:
最后会对标记为 finished 的执行 safelyRemoveOperation;
####CallStoreCacheProcessForOperation
先从 imageContext 中取出 storeCacheType、originalStoreCacheType、transformer、cacheSerializer,判断是否须要存储转换后图像数据、原始数据、等待缓存存储结束:
BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer;
BOOL shouldCacheOriginal = downloadedImage && finished;
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
复制代码
若是 shouldCacheOriginal 为 NO,直接转入 transformProcess。不然,先确认存储类型是否为原始数据:
// normally use the store cache type, but if target image is transformed, use original store cache type instead
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
复制代码
存储时若是 cacheSerializer 存在则会先转换数据格式,最终都调用 [self stroageImage: ...]
。
当存储结束时,转入最后一步,transformProcess。
####CallTransformProcessForOperation
转换开始前会例行判断是否须要转换,为 false 则 callCompletionBlock 结束下载,判断以下:
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer;
BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
复制代码
若是须要转换,会进入全局队列开始处理:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
if (transformedImage && finished) {
/// 1
} else {
callCompletionBlock
}
}
});
复制代码
转换成功后,会依据 cacheData = [cacheSerializer cacheDataWithImage: originalData: imageURL:];
进行 [self storageImage: ...]
存储图片。存储结束后 callCompletionBlock。
若是你能看到这里,仍是颇有耐心的。但愿你们看完可以大概了解 SD 的 work-flow,以及一些细节上的处理和思考。在 SD 5.x 中,我的感觉最多的是其架构的设计值得借鉴。
最后,这篇其实还少了 SDImageCoder,这个留到下一篇的 SDWebImage 插件及其扩展上来讲。