前言
SDWebImage是我们做iOS开发的时候一个比较常用的图片缓存加载库,对于一个优秀的三方库,就很有必要对它的源码进行阅读和学习,学习优秀的源码,还有助于提高我们的实力。
系统结构
一个优秀的框架应当具备"把简洁留给别人, 把复杂留给自己"
特性,来看下它的系统结构设计
由系统结构图,将SDWebImage
可以分为两类:
- 核心类
SDWebImageManager
提供加载&取消图片以及缓存处理等,提供了加载图片的统一接口SDImageCachesManager
负责SDWebImage的整个缓存工作,提供了缓存存储、删除、查找等功能。SDImageLoaderManager
提供全局image loader管理SDImageCodersManager
提供图片的解码工作,编码器数组是一个优先级队列,后面添加的编码器将具有最高优先级SDWebImageDownloader
图片的下载中心,管理图片的下载队列
- 工具类
UIButton+WebCache
支持UIButton加载图片的工具类NSData+ImageContentType
根据图片数据获取图片的类型,比如GIF、PNG等UIImage+MultiFormat
根据UIImage的data生成指定格式的UIImageUIImage+GIF
传入一个GIF的NSData,生成一个GIF的UIImageUIView+WebCache
所有的UIView及其子类都会调用这个分类的方法来完成图片加载的处理,同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作SDAnimatedImageView+WebCache
提供SDAnimatedImageView.h加载GIF的能力(需使用SDAnimatedImageView)
SDWebImage加载图片时序
SDWebImage
加载图片的顺序如下:
- 组件调用
sd_setImageWithURL
, 最终都会执行到UIView+WebCache
的sd_internalSetImageWithURL:
方法
- (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 {
// 这里复制了一个context对象,避免外部改变影响到
if (context) {
// copy to avoid mutable object
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;
// 如果options没有包含SDWebImageDelayPlaceholder(需要延时加载占位图)
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
if (url) {
// reset the progress(重置progress进度)
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// check and start image indicator
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// 这里是一个处理循环引用的逻辑
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
// 处理加载进度
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
// 加载图片的逻辑
@weakify(self);
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) {
@strongify(self);
if (!self) { return; }
// if the progress not been updated, mark it to complete state
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// check and stop image indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!self) { return; }
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = self.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
callCompletedBlockClojure();
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
dispatch_main_async_safe(^{
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}
- 进入到
SDWebImageManager
的loadImageWithURL:options:
方法 - 判断是否是黑名单url(多次加载失败就进入黑名单), 如果是则直接返回
Image url is blacklisted
-
进入到
callCacheProcessForOperation:
执行queryImageForKey
方法查找缓存// Query normal cache process - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // 获取要使用的图像缓存对象 id<SDImageCache> imageCache; if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) { imageCache = context[SDWebImageContextImageCache]; } else { imageCache = self.imageCache; } // 获取要使用的图像缓存类型(内存、磁盘) SDImageCacheType queryCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextQueryCacheType]) { queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue]; } // 查找缓存 BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { // 进入源码看到SDWebImage的缓存默认key是依据url来的, 但它也支持自定义SDWebImageCacheKeyFilter 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); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url]; [self safelyRemoveOperationFromRunning:operation]; return; } else if (context[SDWebImageContextImageTransformer] && !cachedImage) { // Have a chance to quary original cache instead of downloading [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock]; return; } // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock]; }]; } else { // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; } }
-
进入到
queryCacheOperationForKey
方法查找缓存/** * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediatelly. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a NSOperation instance containing the cache op */ - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
如果开启了内存缓存,那么先从内存里面查找缓存
SDWebImage缓存设计
SDWebImage
的图片缓存采用的是 Memory
和 Disk
双重Cache机制,通过SDImageCache
来统一实现,其中Memory
缓存是通过SDMemoryCache
来实现的,Disk
缓存是通过SDDiskCache
来实现的。
SDMemoryCache
继承自NSCache
,是线程安全的。
@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock;
@end
可以看到定义了一个NSMapTable
类型的weakCache
以及一个信号量的锁.
缓存存储
// `setObject:forKey:` just call this with 0 cost. Override this is enough
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
// Store weak cache
SD_LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];
SD_UNLOCK(self.weakCacheLock);
}
}
在添加缓存的时候,会先调用一下父类NSCache的setObject
方法,然后再实现自身weakCache
的缓存实现,为什么既然有了NSCache的缓存之后,这里还要再重复的实现一次缓存呢?
因为NSCache在收到内存警告时是无序释放内存的,并不是我们期望的先进先出形式,在实际情况下往往是新数据再次被利用的情况会很大,所以作者又加了一层缓存,并且做了一个开关shouldUseWeakMemoryCache
控制,以此来提高缓存的命中率
缓存读取
- (id)objectForKey:(id)key {
// 先从NSCache当中读取缓存
id obj = [super objectForKey:key];
// 如果没有开启shouldUseWeakMemoryCache缓存
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];
}
// 重新把weakCache缓存存储到NSCache当中
[super setObject:obj forKey:key cost:cost];
}
}
// 返回weakCache取到的缓存
return obj;
}
NSMapTable
是存储可变元素的一种方式,可以通过弱引用
来持有keys和values, 当key或者value被deallocated的时候,所存储的实体会被移除
SDDiskCache
SDDiskCache
继承自NSObject
,通过NSFileManager
来实现磁盘缓存
- 磁盘缓存是通过
NSFileManager
直接将图片存放到文件夹下面的
- (void)setData:(NSData *)data forKey:(NSString *)key {
NSParameterAssert(data);
NSParameterAssert(key);
// 判断将要缓存的路径是否存在,如果不存在,则创建一个
if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
[self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 获取默认的缓存路径
NSString *cachePathForKey = [self cachePathForKey:key];
// 将路径转为url
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
// 将图片数据写入文件,并保存
[data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
// 禁用icloud备份,默认是YES
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
- 磁盘缓存的key是MD5(url)+图片格式得到的
static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key) {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
// File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
ext = nil;
}
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
// 所以最后的图片保存路径就是 "沙盒cache路径"+"url的md5吗"+".图片类型"
return filename;
}
其它关于缓存的函数,移除过期的缓存或当缓存到最大值时移除较早的图片
- (void)removeExpiredData {
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// Compute content date key to be used for tests
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
case SDImageCacheConfigExpireTypeCreationDate:
cacheContentDateKey = NSURLCreationDateKey;
break;
case SDImageCacheConfigExpireTypeChangeDate:
cacheContentDateKey = NSURLAttributeModificationDateKey;
break;
default:
break;
}
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 最早的有效缓存的时间,小于这个时间的缓存都失效了
NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
// 当前所有缓存的大小
NSUInteger currentCacheSize = 0;
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
// 存储需要移除的缓存图片的路径
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 错误处理
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 通过时间来判断出需要移除的缓存
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 计算有效缓存大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
// 移除缓存
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
NSUInteger maxDiskSize = self.config.maxDiskSize;
// 当缓存大小大于设置的最多缓存控件时,移除相对较早缓存
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
// 只留下剩下最大缓存的一半,其它全部清除了
const NSUInteger desiredCacheSize = maxDiskSize / 2;
// 通过缓存的时间来排序,才好移除早期的缓存
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// 开始清除缓存
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
// 当剩下的缓存小于最大缓存的一半时,停止缓存清除
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
}
SDWebImage是通过removeExpiredData函数来移除过期的缓存或当缓存到最大值时移除较早的图片 当应用程序进入后台,会调用applicationDidEnterBackground函数执行移除 当应用程序终结的时候,会调用applicationWillTerminate函数执行移除
缓存配置
SDWebImage
为我们提供了一个专门配置的对象SDImageCacheConfig
来设置内存缓存、缓存最大容量等
@interface SDImageCacheConfig : NSObject
// 是否对图片进行解压缩 默认 YSE
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 是否禁用icloud备份 默认 YSE
@property (assign, nonatomic) BOOL shouldDisableiCloud;
// 是否内存缓存 默认 YSE
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
/**
* The reading options while reading cache from disk.
* Defaults NSDataReadingMappedIfSafe
*/
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
/**
* The writing options while writing cache to disk.
* Defaults NSDataWritingAtomic
*/
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
// 缓存的超时时间
@property (assign, nonatomic) NSInteger maxCacheAge;
// 最大缓存容量
@property (assign, nonatomic) NSUInteger maxCacheSize;
@end
它是一个单例对象,只需我们再下载图片之前取到SDImageCache单例,就可以对其参数进行设置,如下
// 如果这几行代码写在 didFinishLaunchingWithOptions 里面,那么就可以对所有的图片下载进行设置
[SDImageCache sharedImageCache].config.maxCacheAge = 60 * 60 * 24 * 7; // 磁盘缓存 7天
[SDImageCache sharedImageCache].config.maxCacheSize = 0; // 磁盘缓存 这里设置为0,表示无限大
[SDImageCache sharedImageCache].config.shouldCacheImagesInMemory = true; // 开启内存缓存
[SDImageCache sharedImageCache].maxMemoryCost = 0; // 内存缓存 最大值
[SDImageCache sharedImageCache].maxMemoryCountLimit = 0; // 内存缓存 最大数量
本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.