SDWebImage 是一个为苹果各个平台提供图片下载和缓存操作的开源库。相信只要从事 iOS 开发就算没用过也至少听说过。所以阅读它的码源并进行分析,对于开发者尤其是对组件封装不太熟悉的,应该有所裨益。
从 UML 图和时序图说起
从 UML 图看类结构
这张是 SDWebImage 的 UML 图,我在图中做了些基本关系说明。
从这张图来分析,SD 通过分类给常见的 View 提供下载接口。其内部的结构主要涉及到以下几个大类:
SDWebImageManager
: 控制中心,管理图片的下载和缓存
SDWebImageDownloder
: 下载中心,对所有的下载操作进行管理
SDWebImageDownloderOperation
: 下载操作,继承 NSOperation 处理单个图片的下载
SDImageCache
: 缓存中心,负责对图片的内存和磁盘缓存进行管理
SDWebImageCodersManager
: 图片压缩编码,将图片按照格式进行压缩,转换成 NSData
SDWebImagePrefetcher
: 预下载图片,负责处理需要预下载的图片,下载等级是 SDWebImageLowPriority
从图中感受到的一点就是,分工明确,各司其职。暴露给外部的接口简洁清晰。
从时序图看执行逻辑
从时序图中可以看出 SDWebImage 的执行逻辑是,外部提供 URL ,之后 Manager 根据 URL 在缓存中查找,是否有相应的图片。如果有,则将图片返回给外部。如果缓存中没有图片或者外部要求直接从网络下载图片,则从网络异步下载图片。图片下载完成之后,异步存储到内存和磁盘,同时返回给外部。
从 UML 和 时序图,可以很直观的看出整个项目的架构和内部逻辑,自己以后在封装组件的时候也应该试着画一画,不但方便别人理解,自己也能从更宏观的角度看整个组件,往往能找可以优化的点,甚至是新的设计思路。
SDWebImage 下载和缓存
SD 最核心的内容就是其下载和缓存有下列特性,同时提供的接口又简洁清晰,所以才如此受大家欢迎。
- 使用分类实现 UIImageView, UIButton, MKAnnotationView 的图片下载以及缓存管理
- 图片异步下载
- 异步缓存图(内存和磁盘),同时有相应的移除策略
- 后台进行图片解压
- 对 url 进行校验,同样的 url 不会多次重复下载
- 对失败的 url 进行校验,不会重复多次去执行下载操作
- 不会阻塞主线程
下面就从码源分析,以上的特性是如何实现的。
SDWebImageManager 控制中心
SDWebImageManager
提供了一个单利对象,用来统一控制下载和缓存操作。操作中主要涉及的类和方法如下:
SDWebImageOptions
: 枚举类来控制下载操作中的配置,主要配置下载的优先级,策略,缓存策略等
SDWebImageCombinedOperatio
: 一个操作对象,负责一个图片的下载和缓存
SDWebImageCacheKeyFilterBlock
: 允许用户根据 Url 自定义缓存的 Key
SDWebImageManagerDelegate
: 提供下载图片之前和之后的回调
当外部需要根据 Url 获取一张图片,会调用下面方法
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 没有定义完成回调,期望这种下载操作通过 SDWebImagePrefetcher 预下载完成。
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 如果外部不小心传入的是 string 转换为对应的 url
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止因为因为类型错误,引发崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 创建一个操作对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (url) {
//检测该 Url 是否是之前下载失败的 Url,通过 @synchronized 加锁同步操作 _failedURLs
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// url 错误,或者该 url 是失败 url 且不要求重试失败 url ,则直接返回
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;
}
// 在操作数组中,加入当前操作
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
// 获取当前缓存配置
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
// 根据 options 的配置,从缓存中读取 image
__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
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 缓存没有获取到图片,或者外部要求从网络刷新图片,则进行下载操作
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) {
// 如果缓存中取得了图片,并且要求刷新图片,则先返回缓存中的图片给外部,之后继续从网络下载并刷新缓存中的图片
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 获取当前下载配置
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;
}
// 下载完成操作 block 回调,需要再一次 weak-strong 操作,避免循环引用。
__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) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
} else if (error) {
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
//下载失败,并且排除上述网络原因,将该 Url 置为操作失败的 Url
[self.failedURLs addObject:url];
}
}
}
else {
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
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
// 设置了 NSUrlCache 作为默认缓存,则不执行自定义的缓存
} 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];
// pass nil if the image was transformed, so we can recalculate the data from the image
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) 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) {
[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];
}
}];
} else if (cachedImage) {
//直接返回缓存中的 image
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 缓存中没有 image ,也不允许从网络加载
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
return operation;
}
SDWebImageCombinedOperation
遵循 SDWebImageOperation
协议,实现了取消操作方法。
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
//取消缓存操作
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
//取消下载操作
if (self.downloadToken) {
[self.manager.imageDownloader cancel:self.downloadToken];
}
//将操作移除
[self.manager safelyRemoveOperationFromRunning:self];
}
}
SDWebImageManager
的核心就是这个,统筹负责下载和缓存的协调。
下载
SD 的下载主要由 SDWebImageDownloader
和 SDWebImageDownloaderOperation
这两个类来实现。前者是一个单例,统筹所有的下载操作,后者继承自 NSOperation
自定义了下载操作。
SDWebImageDownloader 下载中心
SDWebImageDownloader
主要负责图片的下载控制,主要涉及下面的属性和方法:
SDWebImageDownloaderOptions
: 枚举下载配置,配置来源是 SDWebImageOptions
中的下载配置
SDWebImageDownloaderExecutionOrder
: 下载顺序,有 FIFO 的队列形式和 LIFO 的栈形式
SDWebImageDownloadToken
: 单个下载操作记号
maxConcurrentDownloads
: 并发的最大下载数
urlCredential
: 处理 url 证书信息
SDWebImageManager
进入下载环节之后,会调用下列方法:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// 返回给外部一个 SDWebImageDownloadToken,可以取消该次下载操作
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
//返回一个下载 operation
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 避免重复缓存,确认使用的是默认的 NSURLCache 策略,还是自定义的缓存策略
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//管道下载技术
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
//解压图片,会展示高质量的图片,但是会增加内存消耗
operation.shouldDecompressImages = sself.shouldDecompressImages;
//url 证书信任
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
// LIFO 的情况下,将前一个下载操作依赖当前操作,实现 LIFO 逻辑,在这种情况下设置 NSOperationQueue 的最大并发数并没有效果,按照依赖来
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
上面的下载方法,在 block 中定义了怎么创建当前下载的 operation ,对 operation 及其回调的操作在下列方法中。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
//加锁同步操作
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
if (!operation) {
//调用 block 创建 operation
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
//操作完成,加锁同步移除当前操作
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
}
UNLOCK(self.operationsLock);
//创建当前下载操作的回调的字典, 该字典类型在 SDWebImageDownloaderOperation 中申明且未公开,所以这里使用 id ,外部不需要关心具体的类型
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
上面添加回调的方法中加锁的实现采用了信号量来构建
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
使用信号量来做的主要原因是,性能比其他加锁方式高;
@synchronized 这种加锁方式开销大,但代码简单易使,比较适合用来做一些简单的操作。
SDWebImageDownloadToken
遵循并实现了 SDWebImageOperation
协议
- (void)cancel {
if (self.downloadOperation) {
//取消当前下载操作, SDWebImageManager 的当前 combinedOperation 调用 cancel 的时候会调用到这
SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
if (cancelToken) {
[self.downloadOperation cancel:cancelToken];
}
}
}
SDWebImageManager
和 SDWebImageDownloader
都用一个单独的类根据 url 来管理当前的 operation ,实现取消的方法。这样就将取消操作部分逻辑剥离出来,比较清晰明了。
SDWebImageDownloaderOperation 下载操作
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
用来存放 progress 和 complete 回调的字典。SDWebImageDownloadToken
的属性 downloadOperationCancelToken
真正的类型就是这个。
SDWebImageDownloaderOperation
继承自 NSoperation
,关于自定义 NSOperation
在这里不做阐述。需要的人可以参考这篇文章iOS 并发编程之 Operation Queues
这里着重讲下里面的 start
方法,主要下载操作的逻辑都在这里。
- (void)start {
@synchronized (self) {
//检查操作是否被取消
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#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 (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
// 采用的是 NSURLCache 缓存的话,先获取之前的缓存,之后对比
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
[self.dataTask resume];
if (self.dataTask) {
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 {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
}
#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
}
下面说下取消下载任务方法:
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
/*
使用栅栏来做线程同步,因为是同步执行,barrierQueue 是并行队列,这里的逻辑是等到 block 里面的执行完毕,才继续下一步。
值得注意的是,这里不能用串行队列,如果这样会造成死锁
**/
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.dataTask) {
[self.dataTask cancel];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
});
// As we cancelled the task, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
// 修改操作状态, 在他们的 setter 方法中,会发送 KVO 通知
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
- (void)reset {
__weak typeof(self) weakSelf = self;
dispatch_barrier_async(self.barrierQueue, ^{
[weakSelf.callbackBlocks removeAllObjects];
});
self.dataTask = nil;
NSOperationQueue *delegateQueue;
if (self.unownedSession) {
delegateQueue = self.unownedSession.delegateQueue;
} else {
delegateQueue = self.ownedSession.delegateQueue;
}
if (delegateQueue) {
NSAssert(delegateQueue.maxConcurrentOperationCount == 1, @"NSURLSession delegate queue should be a serial queue");
[delegateQueue addOperationWithBlock:^{
weakSelf.imageData = nil;
}];
}
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
缓存
如果外部没有特殊需求,默认会走 SD 自定义的缓存策略,而不是 NSUrlCache。
SDImageCache 缓存中心
SDImageCache
是一个单例,来操作所有缓存。
初始化
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// Create IO serial queue
// 创建了一个串行队列,用于操作本地缓存
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
_config = [[SDImageCacheConfig alloc] init];
// Init the memory cache
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if SD_UIKIT
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
存储图片
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
/*
ARC 不使用于 Core Graphics 和 Core Text 这类 C 的库,需要添加自定释放池
**/
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
[self _storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
存储操作时有用到自动释放池,哪些时候需要使用自动释放池,可以根据 苹果的文档 来判断。
查询
内存查询主要根据 key 来检索有没有图片,磁盘查询主要检索有没有根据 key 生成的 path 路径相关文件的存在。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
//可能有大量的 image 图片需要查询,在一个 RunLoop 循环中会产生很多临时对象在 Autorelease Pool 中不能释放,使用 @autorelease{} 来及时释放对象,避免内存占用过高。
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = image;
if (!diskImage && diskData) {
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
移除策略
当内存发出警告 SD 会清理内存,
当 APP 将被挂起或者杀死,SD 都会查询一遍本地缓存,如果本地缓存超出设置的容量,则 SD 会相应的清理本地磁盘。
//NOTE 先删除超过存储时限的文件,要是还超过最大容量,则按照时间正序删除文件至最大容量的一半
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
//NOTE 获取缓存文件相关属性(文件路径,最后修改日期,文件大小)
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
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];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[_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.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray<NSURL *> *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 ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
总结
以上就是 SD 阅读的主要记录,当然还有很多细节和旁支没有写出,之后会再对个人觉得 SD 中好的点做一个记录。同时如果你发现上文中有什么不明白或者不对的地方,都欢迎与我交流,相互成长😁。