SDWebImage
UIView category
我们从一个常用的调用接口开始探索:
let url = URL(string: "...")
imageView.sd_setImage(with: url)
这是 UIImageView+WebCache
提供的其中一个接口,由于 Objective-C 不像 Swift 可以为函数声明默认参数值,因此需要声明多个接口。它们最终会汇合到这个方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
context:context
setImageBlock:nil
progress:progressBlock
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
if (completedBlock) {
completedBlock(image, error, cacheType, imageURL);
}
}];
}
首先理解这个接口各参数的含义。
SDWebImageOptions
定义在 SDWebImageDefine.h
,是图片下载的选项,用 bitmask 的形式表示:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
* By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
* This flag disable this blacklisting.
*/
SDWebImageRetryFailed = 1 << 0,
}
SDWebImageContext
定义在 SDWebImageDefine.h
,是对下载选项的一个扩充,This hold the extra objects which options enum can not hold. 它是一个字典,可以对图片 operation key、cache、loader、coder、transformer 等进行自定义。
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
sd_internalSetImage
多了一个参数 @param setImageBlock
If not provide, use the built-in set image code (supports UIImageView/NSImageView
and UIButton/NSButton
currently).
查看分类 @interface UIView (WebCache)
,这个分类中为 UIView
添加了几个属性:
@property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL;
@property (nonatomic, strong, readonly, nullable) NSString *sd_latestOperationKey;
@property (nonatomic, strong, null_resettable) NSProgress *sd_imageProgress;
为分类添加属性的方法:
- (nullable NSURL *)sd_imageURL {
// selector 作为 key!
return objc_getAssociatedObject(self, @selector(sd_imageURL));
}
- (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL {
objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
接下来探索 @implementation UIView (WebCache)
中关键方法的实现。
下载图片的操作 SDWebImageOperation
会与当前 UIView
关联起来:
- (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 {
// 注意,这个函数是众多 UIView 接口背后的调用
// 其中 context 参数是 SDK 外部用户直接传参进来的
// 为了避免修改外部的参数,因此对它执行拷贝
if (context) {
// copy to avoid mutable object
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]); // 例如 "UIImageView"
}
self.sd_latestOperationKey = validOperationKey;
// 取消原先的操作
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
if (url) {
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
dispatch_main_async_safe(^{
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
callCompletedBlockClosure();
});
}];
// 将这个 operation 与当前 UIView 实例关联起来!
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
}
NSMapTable
关联 operation 定义在 UIView+WebCacheOperation
,其本质是用一个字典存储当前 UIView 正在运行的操作 [operationKey: operation],有趣的是这里用到了 NSMapTable
,它的初始化:
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
NSMapTable 类似于 NSDictionary,但它可以指定 key 和 value 的内存管理语义。
NSDictionary / NSMutableDictionary 对 key 拷贝,对 value 强引用 。
NSMapTable 可以对键和值弱引用,当键或值当中的一个被释放时,这一键值对就会被移除掉。
NSDictionary 的 key 需要遵守 NSCopying 协议,当调用 NSMutableDictionary 的 - (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
方法时,会对 key 进行 copy。
留意 SDWebImageContextSetImageOperationKey 是可以由外部使用者设置的任意对象,这些对象不一定遵循 NSCopying,因此这里将 key 的内存管理语义指定为 NSPointerFunctionsStrongMemory。
同时,因为这里对 operation 是弱引用,当 key 对应的 operation 执行完毕被释放后,字典里的这一项也会自动被移除。
SDWebImageManager
接下来研究 [SDWebImageManager sharedManager]
和 loadImageWithURL:
接口。
SDWebImageManager 这个类是 UIView category 提供的接口背后真正完成工作的类。当然,我们也可以不通过 UIView 的接口而直接使用它。它结合了下载和缓存的两部分功能,它的初始化:
- (nonnull instancetype)init {
id<SDImageCache> cache = [[self class] defaultImageCache];
if (!cache) {
cache = [SDImageCache sharedImageCache];
}
id<SDImageLoader> loader = [[self class] defaultImageLoader];
if (!loader) {
loader = [SDWebImageDownloader sharedDownloader];
}
return [self initWithCache:cache loader:loader];
}
这里的 cache 和 loader 可以由外部自行指定,如果没有指定则使用框架内的默认实现类。
一、组合操作
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 组合操作:下载+缓存!
// 遵循 SDWebImageOperation 协议,operation 可以被 cancel,因此在后续的每一步骤中,都有对 operation 状态的检查
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// Preprocess the options and context arg to decide the final the result for manager
// 处理自定义项的优先顺序:
// self.optionsProcessor > 接口传参 context > self.transformer/cacheKeyFilter/cacheSerializer > default
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// Start the entry to load image from cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
自定义项的优先顺序处理,类似于 Git Configuration 的分级处理,高优先级的选项会覆盖低优先级的选项(仓库 -> 用户 -> 系统)。
Git uses a series of configuration files to determine non-default behavior that you may want. The first place Git looks for these values is in the system-wide [path]/etc/gitconfig file, which contains settings that are applied to every user on the system and all of their repositories. If you pass the option --system to git config, it reads and writes from this file specifically.
The next place Git looks is the ~/.gitconfig (or ~/.config/git/config) file, which is specific to each user. You can make Git read and write to this file by passing the --global option.
Finally, Git looks for configuration values in the configuration file in the Git directory (.git/config) of whatever repository you’re currently using. These values are specific to that single repository, and represent passing the --local option to git config. If you don’t specify which level you want to work with, this is the default.
Each of these “levels” (system, global, local) overwrites values in the previous level.
二、查找缓存流程
- (void)callCacheProcessForOperation:
- 查找结果返回,不管是否命中,都进入下载流程
- 指定 SDWebImageFromLoaderOnly,不需要缓存,进入下载流程
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
if (shouldQueryCache) {
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation); // 解除 operation 引起的循环引用!
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
}
}
三、下载流程
- (void)callDownloadProcessForOperation:
- 需要下载
- 命中缓存的情况下,下载完成,刷新缓存!
- 未命中缓存的情况下,下载完成,写入缓存!
- 不需要下载,返回缓存图片
- Image not in cache and download disallowed by delegate
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 缓存命中
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// context 是不可变的,要想改变它需要经过以下的操作
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
@weakify(operation);
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
// Continue store cache process
// 下载完成,写入缓存!
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
}
四、写入缓存流程
- (void)callStoreCacheProcessForOperation:
- 缓存原始图像
- 要缓存到磁盘,先经过 cache serializer 处理
- 缓存完成后,进入 transform 流程
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
BOOL shouldCacheOriginal = downloadedImage && finished;
// 有原始图像是一定会保存的
if (shouldCacheOriginal) {
if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
// 要缓存到磁盘,先经过 cache serializer 处理
// The cache serializer is used to convert the decoded image, the source downloaded data, to the actual data used for storing to the disk cache.
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
// 调用 [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:...];
[self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
// Continue transform process
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
});
}
}
}
五、transform 流程
缓存 transform 后的图片,完成全流程。
// Transform process
- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
originalData:(nullable NSData *)originalData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
if (transformedImage && finished) {
[self storeImage:transformedImage imageData:cacheData forKey:key cacheType:storeCacheType options:options context:context completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}];
}
}
});
}
}
框架提供了圆角、Resize、Cropping、Flipping、Rotate、Tint、Blur、Fliter 多种 transformer,还可以自由组合成 SDImagePipelineTransformer;通过 UIImage+Transform
分类提供实现。
下一步,我们分别探索 imageCache(查找缓存、写入缓存)和 imageLoader(下载)的实现。
SDImageCache
SDImageCacheDefine
定义了协议 @protocol SDImageCache
,外部可以遵守这个协议并自行实现协议中的方法。如果不自定义的话,则会使用默认的实现类 SDImageCache
。
我们知道 SDWebImageCombinedOperation
包含了两个操作 cacheOperation
和 loaderOperation
,其中 cacheOperation
就是从这里创建的:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
// First check the in-memory cache...
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
// [self.memoryCache objectForKey:key];
}
// 如果指定只从内存中查找缓存,到这里就结束了
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil; // 已完成查询,operation 返回 nil
}
// Second check the disk cache...
NSOperation *operation = [NSOperation new];
// block,取磁盘缓存可以是同步或异步执行
void(^queryDiskBlock)(void) = ^{
// 操作被取消的话这个 block 不会被执行
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
// [self.diskCache dataForKey:key];
UIImage *diskImage;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
} else if (diskData) {
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
// 在这里完成对图片的解码!
// 见 SDImageCacheDefine.m
// UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
// return image;
// 解码后的图片缓存到内存中!
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
}
};
return operation;
}
在 SDMWebImageManager 第四步写入缓存流程时,会调用到 SDImageCache 的这个方法:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
// 对文件的读写要放到同一个操作队列里
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// 没有原始 data,尝试获取图片格式,并进行编码
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
// 写入磁盘
[self _storeImageDataToDisk:data forKey:key];
}
});
}
}
我们接着探索:内存缓存如何写入/读取?磁盘缓存如何写入/读取?图片如何解码?
SDMemoryCache
SDWebImage 声明了 @protocol SDMemoryCache <NSObject>
协议并提供了默认实现。在框架中处处体现这样的面向协议编程设计思想。
NSCache
SDMemoryCache 继承自 NSCache,它有几个特点:
- 自动回收内存,当内存紧张时,自动移除键值对。
- 线程安全,操作无需加锁。
- 对 key 强引用,而不是 copy。
我们可以指定 NSCache 中的 countLimit
(The maximum number of objects the cache should hold) 和 totalCostLimit
(The maximum total cost, such as the size in bytes of the object, that the cache can hold),来控制缓存的容量,这两个属性默认值是 0,即无限制。
WeakMemoryCache
注意在 SDImageCacheConfig 中有一个选项 shouldUseWeakMemoryCache 默认开启。它的说明:SDMemoryCache
will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time. 当图片存入内存缓存的同时,也在这个哈希表里保存了一份弱引用。
When memory warning is triggered, since the weak maptable does not hold a strong reference to image instance, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground. 当内存紧张,内存缓存被清除时, 有一些图片仍然会被正在显示的 UIView 强引用,这些内存不会回收,这时 weak memory cache 就起作用了。可以在下次查找缓存时直接命中,而无需读取磁盘。
它同样使用 NSMapTable 实现,对它的操作需要加锁:
- (void)commonInit {
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
self.weakCacheLock = dispatch_semaphore_create(1);
}
小结
- 写入内存缓存:调用 NSCache 和 NSMapTable 的
setObject:forKey:
方法。 - 读取内存缓存:调用 NSCache 的
objectForKey:
方法,如果未命中,查询 NSMapTable。如果弱引用缓存命中,会同步到 NSCache 中! - 擦除内存缓存:调用 NSCache 和 NSMapTable 的
removeObjectForKey
方法。
SDDiskCache
SDWebImage 声明了 @protocol SDDiskCache <NSObject>
协议并提供了默认实现。在框架中处处体现这样的面向协议编程设计思想。
查找磁盘缓存:
- (NSData *)dataForKey:(NSString *)key {
NSString *filePath = [self cachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
return data;
}
写入磁盘缓存:
- (void)setData:(NSData *)data forKey:(NSString *)key {
if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
[self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
NSString *cachePathForKey = [self cachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
// ignore iCloud backup resource value error
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
SDDiskCache 还实现了计算文件占用大小、文件个数的相关方法。