作者 | 土土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 插件及其扩展上来说。
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ATyNGqYDz--5CUtd8WCp6A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。