SDWebImage这个第三方库有多厉害,从它的GitHub上过万的Star就可以看出来。一直以来都想好好拜读它的源码,但之前每次都看得头昏脑胀的,最后都是不了了之。方知武侠小说中修为没到,强练绝世秘籍会导致走火入魔的说法并不是无稽之谈。
最近项目没有这么紧张,又静下心来,好好研读了几遍。终于看出了一点点门道,所以写篇笔记记录一下。话不多说,进入正题。先来一张流程图压压惊:
本文采用讲解主体逻辑,贴出源码,并在源码中添加注释的方法;同时会把比较有特色的点,结合自己的理解,稍作分析。作为iOS码农界的小学生,能力有限,水平一般(脑补郭德纲的声音。。。)。如有不对之处,还望指正。
为已有的类添加方法,毫无疑问应该首先想到类别(Category)这种方法。那直接进入到UIImageView+WebCache.m文件中,看到一系列的方法,其实最终都是走到了UIView+WebCache.m的的这个方法中:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary *)context;
为什么要这么设计呢?因为前面流程图上说了,不光UIImageView有扩展,UIButton也有扩展方法,那么把最终的实现放到他们的共同父类UIView的类别中,也就顺理成章了。方法实现中,一进来是这么两行代码:
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
这个validOperationKey其实就是为了缓存或者从缓存中查找operation当作key用的;如果外部没有传入的话,就默认的取类名。先用这个key取消可能正在操作的operation,避免后续的回调混乱,保证这个Imageview或者Button只存在一个请求图片的操作。进入sd_cancelImageLoadOperationWithKey:方法的内部,可以看到是在UIView+WebCacheOperation.m中实现的。这里将key和operation映射保存在动态绑定的SDOperationsDictionary中,名字是dictionary,实际上用到的是NSMapTable。类似于字典,但比字典更灵活的一个类。这篇文章说的比较详细。
接下来是动态绑定url到当前对象上;如果设置的options不是延迟设置占位图的话,就在主线程回调设置占位图:
//动态绑定url到当前对象上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果设置的options不是延迟设置占位图的话,就在主线程回调设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
接下来的这个最外层的if else的大逻辑就是,如果传入的url不为nil的话,就走下面一大段逻辑;否则发起错误回调。看看url不为空后续的逻辑,对照着注释应该比较清楚:
#if SD_UIKIT
// 检查activityView是否可用
if ([self sd_showActivityIndicatorView]) {
//添加菊花控件
[self sd_addActivityIndicator];
}
#endif
// 初始化sd_imageProgress的总任务数和任务完成数
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
//取到manager,如果外部传入了就用传入的值
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
//弱引用self,防止引用循环
__weak __typeof(self)wself = self;
//把传进来的progressBlock封装一下,后续生成operation时使用
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
其中的SD_UIKIT宏定义后面会频繁出现,其实就类似一个bool值,在iOS和tvOS中为真,其他系统下为假:
#if TARGET_OS_IOS || TARGET_OS_TV
#define SD_UIKIT 1
#else
#define SD_UIKIT 0
#endif
然后是生成operation的实现:
id operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];
#endif
...
代码太长了没有截完,大体逻辑就是manager使用传进来的url,options,completedBlock,和上面生成的combinedProgressBlock参数,生成一个operation,并把它和key映射保存在动态绑定的SDOperationsDictionary中。我们不管生成operation的细节,先来看看它的回调block实现。
block一进来是对weakSelf的强引用,因为前面对self进行了弱引用。这里说个比较有意思的点。以前最开始看到block实现中这种先weak再strong的做法我其实非常不理解,疑惑的是这样做到底会不会增加该对象的引用计数?如果会的话这跟不做转换有什么区别?当时百度google了一大堆,硬是没有看懂,所以这个问题拖了很久,面试还被问到过。后来有一次突然看到一篇文章里面说,block里面的strong引用weakSelf,是为了防止多线程切换的时候,weakSelf被提前释放了,后续再访问该对象的时候,引起野指针崩溃才这么做的。我突然豁然开朗,原来先weak后strong的目的其实有两个:
- 先weak引用当前对象,是为了让block捕获的对象是一个弱引用的对象。这样就打破了引用循环,防止双方都强引用对方,形成引用循环,导致内存泄漏。
- block内部的strong是为了增加对象的引用计数,保证该对象在block内部是一直存在的,防止在多线程切换的时候对象被提前释放,后续访问导致野指针崩溃。
有时候不得不感叹,能够在特定的时间节点碰到对的人或物,是多么幸运的一件事情。
好了,我们继续来看回调block的实现细节,主要是一些条件判断和回调处理,没有什么值得特别说明的,对照注释应该是挺好理解的:
#if SD_UIKIT
//如果前面添加了菊花控件,这里先移除
[sself sd_removeActivityIndicator];
#endif
// 如果操作完成,没有错误,而且progress没有更新的话,手动将其置为Unknown状态。
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
//如果已经完成,或者options设置了不自动赋值图片选项的话,就需要执行完成回调
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
//如果(图片存在 且 options设置了不自动赋值图片这个选项的话) 或者 (图片不存在 且 options没有设置延迟显示placeholder图片选项)的话,就不要将回调中的image设置给当前控件。
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
//为callCompletedBlockClojure赋值
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// 检查是否需要执行图片转换
SDWebImageTransition *transition = nil;
//如果已经完成 且 (options设置了强制转换选项 或者 缓存类型为SDImageCacheTypeNone,即没有命中缓存,是从网络获取的图片)的话,就取UIView+WebCache.h头文件中定义的sd_imageTransition转换策略。其实是提供了自定义图片转换的功能,SD默认这个属性是nil。
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
//不同宏定义下执行响应的设置方法
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
说完了block回调,我们进入manager生成operation的方法中一探究竟,即这个方法:
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
这个方法的实现代码比较多,我们先把它拆分成以下几个点来逐一研究:
- url参数判断转换
- 判断缓存策略,生成operation的缓存查询操作
- 在operation的缓存查询回调中看是否需要下载
看一下第一部分,我在代码中加了注释,也是比较好理解的:
//先是一个断言,指明完成回调是必要的参数,否则调用这个方法是没有意义的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 防止常见的把NSString当作NSURL传入的错误
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
//operation引用了manager,方便它内部使用。注意operation的manager属性是弱引用,防止引用循环。
operation.manager = self;
//查询是否是之前请求过的但是失败了的url,这里用了信号量做锁,下面会详细说一下
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果url长度为空 或者 (options没有设置失败后重试选项 且 是之前请求失败的url) 的话,调用完成回调,回传error。callCompletionBlockForOperation:方法其实没做什么事,最终使用的是一个安全调用block的宏。
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
这里比较有意思的是,查询是否是之前请求过的失败url时,使用了信号量加锁。纵观整个SD,很多地方都使用了这种方法替代互斥锁来保证线程安全。把信号量当做锁其实用法也比较简单,看manager 的 init 方法里,初始化了两个信号量,都是当做锁来用的。
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
_imageCache = cache;
_imageDownloader = downloader;
_failedURLs = [NSMutableSet new];
_failedURLsLock = dispatch_semaphore_create(1);
_runningOperations = [NSMutableSet new];
_runningOperationsLock = dispatch_semaphore_create(1);
}
return self;
}
//再看LOCK 和 UNLOCK是什么
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
就是初始化一个为1的dispatch_semaphore_t变量,当前operation抢占到资源时,先调用dispatch_semaphore_wait方法将信号量减1,此时dispatch_semaphore_t变量为零,如果再有其他operation想要获取该变量,就只能排队等着,啥时候前一个operation跑完了dispatch_semaphore_signal方法,将信号量加了1。后面的operation才能获取到该信号量进行下一步。当然信号量是一种比较底层的同步机制,不光是当锁用这么简单。这篇文章有各种锁的说明和比较。
接下来是判断缓存策略,生成operation的缓存查询操作:
//先将当前operation添加到正在进行的所有operation的无序集合中,也用到了前面说的信号量加锁
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
//生成后续查询和存储的key,如果用户自定义了生成key的方法,SD就使用用户自定义的,否则默认为url的absoluteString
NSString *key = [self cacheKeyForURL:url];
//获取缓存options
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
//弱引用当前operation
__weak SDWebImageCombinedOperation *weakOperation = operation;
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//如果当前operation不存在或者已经被取消,则将其从正在进行的operations无序集合中安全的移除,也用到信号量加锁
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 如果 (options没有设置只从缓存中获取图片选项) 且 (没有命中缓存 或者 options设置了刷新缓存选项) 且 (manager的delegate没有实现imageManager:shouldDownloadImageForURL代理方法 或者 实现了该方法,返回YES) 的话,就需要下载图片
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 如果命中了缓存,且options设置了刷新缓存选项,那么先执行完成回调,再去请求图片,以刷新NSURLCache的缓存
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 设置后续downloadToken的options
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//设置operation的downloadToken
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
xxx(代码没截完)
这段实现的大体逻辑就是,生成operation以后,设置它的缓存查询,在缓存查询的回调中检查是否需要下载图片,如果需要的话,再设置它的downloadToken。也就是先查询缓存,如果没有命中或者设置了刷新缓存选项的话,就去下载图片。那么我们SDImageCache这个工具类中,缓存查询方法是怎么实现的,对照着注释看应该是比较清楚了:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 先从内存缓存中查询图片,即从SDImageCache的SDMemoryCache类型的memCache属性中查询,SDMemoryCache继承自NSCache,我记得比较早的SD版本,memCache使用的是NSDictionary。NSCache对比字典有什么优势呢?主要有两点,一是NSCache是线程安全的,另一个是NSCache在内存紧张时,会自动清理部分无用数据。
UIImage *image = [self imageFromMemoryCacheForKey:key];
//如果内存中命中了图片,且options没有设置内存中有数据仍旧查询磁盘缓存的选项的话,就直接执行完成回调,并且返回nil。
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//如果需要进入磁盘查询,先设置好它的回调block
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
// 图片是从内存缓存中命中的
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// 图片是从磁盘缓存中命中的
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
//如果SDImageCache的config设置了shouldCacheImagesInMemory属性,那么将从磁盘命中的图片保存到内存中,方便下次使用。SD默认将该属性置为YES
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
//如果options设置的是同步查询,就直接执行完成回调;否则,将回调异步提交到主队列。
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
//如果options设置的是同步查询,就直接执行queryDiskBlock
queryDiskBlock();
} else {
//否则,将block提交到自己的IO队列,SDImageCache初始化时将该队列指定为了串行,只能一个接一个的执行回调
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
可以看到SD使用的是内存和磁盘的二级缓存,先查询内存,如果命中就直接返回,没有命中的话再查询磁盘缓存;如果磁盘缓存命中,默认会将图片设置到内存缓存中,方便下次使用。同时回调有同步和异步两种选择。缓存查询这块的大体逻辑已经讲完了,我们顺便来看看SDImageCache类中,关于缓存清理这部分的实现逻辑。它在初始化的时候就注册了App将要销毁和进入后台的通知,接到通知以后会自动清理内存,调用删除文件的方法:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// 确定文件的查询键,是AccessDate 还是ModificationDate
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
//查询三个信息,是否是文件夹,存入缓存的时间,文件大小
NSArray *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// 枚举当前路径下的所有文件
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//过期时间,SD默认的文件过期时间是一个星期,如果想自定义的话,可以在SDImageCacheConfig中修改
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 这里的for循环有两个目的
// 1. 将每个过期文件的URL添加到urlsToDelete数组中,后续统一移除对应的文件
// 2. 将每个文件的信息跟URL对应,存到cacheFiles字典中,后面会用到
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if ([[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];
}
// 如果用户设置了最大磁盘缓存尺寸,且当前缓存尺寸超过了设置的最大值。注意SD默认是没有设置最大磁盘缓存的。
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// 设置此次的目标尺寸为最大尺寸的一半
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// 根据文件修改时间将其排序,最老的文件在最前面,也就是说最先删除
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
所以每次App进入后台,SD都会检查,如果磁盘中存在过期文件则删除;同时如果用户设置了最大磁盘缓存尺寸,且已经使用的磁盘大小超过了这个阈值,会以最大值的一半作为此次清理的目标,从最老的文件开始删,直到达到目标尺寸。
说完缓存查询这部分,我们回到operation生成downloadToken这里。其实调用的是SDWebImageDownloader这个工具类来生成downloadToken:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
该方法中最主要的是实现了名叫createCallback的回调block,因为其实一个URL对应一个下载操作,如果多个控件使用了同一个URL下载,是没有必要下载多次的。所以SD会在addProgressCallback:completedBlock:forURL:createCallback:方法中判断是否已经存在了当前URL对应的下载操作,不存在的话再调用createCallback创建。创建下载操作其实就是按部就班的设置超时时间(SD默认15秒)、根据缓存策略生成request、设置request的头信息;然后根据request创建对应的SDWebImageDownloaderOperation,然后把SDWebImageDownloader这个工具类的证书验证及操作优先级等属性赋值给它生成的每个SDWebImageDownloaderOperation。在方法的最后,有这样一个if判断:
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 用户可以设置下载操作的执行顺序,如果设置了LIFO(Last In First Out)的话,会将前一个下载操作依赖当前下载操作,保证了最后生成的下载操作会最先执行
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
这里稍微引申一下,iOS中最常用的多线程编程方法应该就是NSOperation和GCD了吧,这里可以看到NSOperation对比GCD的一个优点:添加依赖非常方便。当然还有另外的优点比如提交的操作可以取消,可以设置操作的优先级等。所以要根据不同的应用场景选择最合适的工具。我们接着看addProgressCallback:completedBlock:forURL:createCallback:方法:
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// 因为URL后续会当做保存下载操作的字典查询的key,所以必须保证不为空。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
//同步查询字典中是否存在该url对应的操作
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
//如果不存在 或者 操作已经标记为完成
if (!operation || operation.isFinished) {
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
//operation的完成回调中会将自己从URLOperations字典中移除
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//将progressBlock和completeBlock赋值给cancelToken
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
//生成downloadToken
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
可以看到,从最外层传入的progressBlock 和 completeBlock 最终都赋值给了cancelToken,我们进入该方法看看:
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
就是将两个回调先用字典装起来,然后添加到callbackBlocks数组中。这里我刚开始看的时候有个疑问,不是一个url对应一个下载吗,为什么SDWebImageDownloaderOperation内部的回调会是个数组呢?后来才想明白,就跟上面说的一样,如果多个控件短时间内加载同一个url,先加载的那个控件生成了一个下载操作,后续就没必要再生成下载操作了,但是回调是必须区分的,因为每个控件的完成回调中,会把图片赋值给当前控件。所以内部的回调要用一个数组来装载,图片下载完成以后依次调用每个回调。
最终的下载操作都是SDWebImageDownloaderOperation这个类实现的。我们知道使用NSOperation实现多线程的话,只有两种方法,一是使用它的子类:NSInvocationOperation 或者 NSBlockOperation;另外就是自定义一个类,继承自NSOperation,覆写它的start方法。可以看到SD使用的是后面一个方法。我们看看它的start方法里都做了些什么:
- (void)start {
//一般都是在start方法开始的时候就检测当前操作是否被取消。
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//iOS和tvOS都可以在App进入后台后向系统申请额外的操作时间
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//如果外部没有赋值session给它,那么就自己在内部生成一个,并赋值给ownedSession,
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// SD特别指明了 URLCache 的 cachedResponseForRequest:方法不是线程安全的
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
//设定dataTask的优先级
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
[self.dataTask resume];
//调用progress回调
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
//回调主线程发送开始下载的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
//如果没有生成dataTask,则调用完成回调,并传递error
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
最后,我们再来看看SDWebImageManager中调用imageDownloader工具类生成downloadToken的完成回调中做了什么事情:
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// 如果strongSubOperation为空,或者被取消了,什么都不做
} else if (error) {
//如果产生错误,则执行完成回调,并回传错误
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// 检查是否需要将当前的url放到请求失败的url数组中
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else {
//一切正常的话就走到这里
if ((options & SDWebImageRetryFailed)) {
//如果options设置了SDWebImageRetryFailed选项,就把当前url从failedURLs中移除。因为有可能多次请求一个url,前面请求失败的话,就被添加到这个数组中了。请求成功的时候需要移除。
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// SD自己的manager是默认实现了缩放处理的,如果使用的是用户自己的manager就走下面这步进行缩放处理
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//异步执行图片转换操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//如果用户自定义了图片的缓存处理方法
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
});
} else {
//将图片存入缓存
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//调用最终的完成回调
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
//如果操作完成,将当前操作从保存的正在运行的操作数组中移除
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
至此,SD的所有主流程我们都梳理了一遍。这一路的参数传递及各种block回调,刚开始看的时候确实会比较懵逼。但是只要静下心来,对照着源码耐心研读,最终一定会融会贯通的。当然,SD还有一些其他的模块,我自己也没有仔细去看,就不班门弄斧了。第三方源码的解读确实是比较花时间,特别是想自己写一篇比较全面的总结得时候就更加需要耐心了。一不小心这篇总结就差不多花了我周末两天时间,已经周日下午三点多,是时候抓住周末的尾巴啦~就酱,溜了溜了。。。
参考资料:
https://knightsj.github.io/2018/02/03/SDWebImage%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
https://www.jianshu.com/p/9e97c11aeea9