作者 | 土土Edmond木
本文基于 SDWebImage 5.5.2。重读的原因也是由于发现它的 API 在不断迭代,许多结构已经不同与早期版本,同时也是为了做一个记录。阅读顺序也会依据 API 执行顺序进行,不会太拘泥于细节,更多是了解整个框架是如何运行的。
如果大家有兴趣的,强烈推荐观看官方的推荐的迁移文档,提到了5.0版本的需要新特性,里面详细介绍其新特性和变化动机,主要 features:
可以说,5.0 的变化在于将整个 SDWebImage 中的核心类进行了协议化,同时将图片的请求、加载、解码、缓存等操作尽可能的进行了插件化处理,达到方便扩展、可替换。
协议化的类型很多,这里仅列出一小部分:
作为上层 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
• 判断条件由 isMainThread 改为了 dispatch_queue_t label 是否相等
关于第二点,有一篇 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.
区别就是从判断是否在主线程执行改为是否由主队列上调度。因为 在主队列中的任务,一定会放到主线程执行。
相比 UIImageView 的分类,UIButton 需要存储不同 UIControlState 和 backgrounImage 下的 image,Associate 了一个内部字典 (NSMutableDictionary
所有 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;
这个方法实现很长,简单说明流程:
• copy SDWebImageContext 转为 immutable, 获取其中的 validOperationKey 默认值为 className;
• 执行 sd_cancelImageLoadOperationWithKey 取消上一次任务,保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突;
• 设置占位图;
• 初始化 SDWebImageManager 、SDImageLoaderProgressBlock , 重置 NSProgress、SDWebImageIndicator;
• 开启下载loadImageWithURL: 并将返回的 SDWebImageOperation 存入 sd_operationDictionary,key 为 validOperationKey;
• 取到图片后,调用 sd_setImage: 同时为新的 image 添加 Transition 过渡动画;
• 动画结束后停止 indicator。
稍微说明的是 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 进行图片批量下载,核心方法如下:
/**
* Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property.
* Prefetching is seperate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls.
* Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch.
*/
- (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 是
@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
另外,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
// Request Modifier
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}
值得注意的是 requestModifier 的获取是有优先级的,通过 imageContext 得到的优先级高于 downloader 所拥有的。通过这种方既满足了接口调用方可控,又能支持全局配置,可谓老少皆宜。同理,id
之后会将确认过的 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 内部则是真正的消费着。下载开始、结束、取消都会全局发送对应通知。整个 responseData 处理流程比较清晰。
• 1. 在 didReceiveResponse 时,会保存 response.expectedContentLength 作为 expectedSize。然后调用 modifiedResponseWithResponse: 保存编辑后的 reponse。
• 2. 每次 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 style="-webkit-print-color-adjust: exact;"> weakCache ,并为其添加了信号量锁来保证线程安全。这里的 weak-cache 是仅在 iOS/tvOS 平台添加的特性,因为在 macOS 上尽管收到系统内存警告,NSCache 也不会清理对应的缓存。weakCache 使用的是 strong-weak 引用不会有有额外的内存开销且不影响对象的生命周期。</keytype,>
weakCache 的作用在于恢复缓存,它通过 CacheConfig 的 shouldUseWeakMemoryCache 开关以控制,详细说明可以查看 CacheConfig 的API 声明。先看看其如何实现的:
- (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 目录下。另外就是过期缓存的清理:
• 根据 SDImageCacheConfigExpireType 排序得到 NSDirectoryEnumerator *fileEnumerator ,开始过滤;
• 以 cacheConfig.maxDiskAage 对比判断是否过期,将过期 URL 存入 urlsToDelete;
• 调用 [self.fileManager removeItemAtURL:fileURL error:nil];
• 根据 cacheConfig.maxDiskSize 来删除磁盘缓存的数据,清理到 maxDiskSize 的 1/2 为止。
另外一点就是 SDDiskCache 同 YYKVStorage 一样同样支持为 UIImage 添加 extendData 用以存储额外信息,例如,图片的缩放比例, URL rich link, 时间等其他数据。
不过 YYKVStorage 本身是用数据库中 manifest 表的 extended_data 字段来存储的。SDDiskCache 就另辟蹊径解决了。利用系统 API
也是协议化后的类,负责调度 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;
其 init 中需要说明的就是 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 storage 另一个重要的方法就是 image query 了,这是定义在
- (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 获取 id
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 中:
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 个方法:
• callCacheProcessForOperation
• callDownloadProcessForOperation
• callStoreCacheProcessForOperation
• callTransformProcessForOperation
• callCompletionBlockForOperation
• safelyRemoveOperationFromRunning
分别是:缓存查询、下载、存储、转换、执行回调、清理回调。可以发现每个方法都是针对 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 初始化完,会检查 failedURLs 是否包含当前 url,如果有且 options 为 SDWebImageRetryFailed,直接结束并返回 operation;如果检查通过会将 operation 存入 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
}];
对缓存查询的结果有两种情况需要处理:
当队列执行到任务时,operaton 已被标志为 canceled 状态,结束下载任务;
否则转到 downloadProcess 。
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];
• 通过 SDWebImageFromCacheOnly 决定是否可以仅从 cache 来获取,或者 cacheImage 是否存在;
• 由代理决定是否需要新建下载任务
• 通过 imageLoader 控制能否支持下载任务
如果 shouldDownload 为 NO,则结束下载并调用 callCompletionBlockForOperation 与 safelyRemoveOperationFromRunning。此时如果存在 cacheImage 则会随 completionBlock 一起返回。
如果 shouldDownload 为 YES,在新建任务前,如有取到 cacheImage 且 SDWebImageRefreshCached 为 YES,会将其存入 imageContext (没有则创建 imageContext)。终于开始新建下载任务并将其保存在 combineOperation 的 loaderOperation。
下载结束后回到 callBack,这里会先处理几种情况:
• operation 被 cancel 则抛弃下载的 image、data ,callCompletionBlock 结束下载;
• reqeust 被 cancel 导致的 error,callCompletionBlock 结束下载;
• 图片刷新但是仍名字了 URLCache 则 do nothing;
• errro 出错,callCompletionBlockForOperation 并将 url 添加至 failedURLs;
• 均无以上情况,如果是通过 retry 成功的,会先将 url 从 failedURLs 中移除,调用 storeCacheProcess;
最后会对标记为 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.0 更多的是其结构设计上的很多思路值得借鉴。
• 如何设计一个稳定可扩展的 API 同时能支持安全动态添加参数?
• 如果设计一个解耦可以动态插拔的架构?
不过,这篇其实还少了 SDImageCoder,这个留到下一篇的 SDWebImage 插件及其扩展上来说。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8