一、请求图片
我们用SDWebImage
给UIImageView加载图片最基本的操作:
[imageView sd_setImageWithURL:url];
这行代码经过层层调用,来到下面这个方法:
- (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 {
SDInternalSetImageBlock internalSetImageBlock;
if (setImageBlock) {
internalSetImageBlock = ^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (setImageBlock) {
setImageBlock(image, imageData);
}
};
}
[self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey internalSetImageBlock:internalSetImageBlock progress:progressBlock completed:completedBlock context:context];
}
这里对setImageBlock
进行了一层包装,再继续往下走,来到了最终的加载图片的方法。
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary *)context {
......
这个方法主要做了以下事情:
- 取消之前的图片下载
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
这里利用operationKey
来取消之前的任务,如果外部没有传来operationKey,则默认使用类的名称。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
这个方法是在UIView + WebCacheOperation
这个分类里面实现的。
SDOperationsDictionary
是NSMapTable
:
typedef NSMapTable> SDOperationsDictionary;
其中value是弱引用。SDOperationsDictionary
的实例通过关联对象保存在UIView中:
static char loadOperationKey;
......
- (SDOperationsDictionary *)sd_operationDictionary {
@synchronized(self) {
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
- 保存url
......
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
......
- 设置占位图
......
dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];
if (!(options & SDWebImageDelayPlaceholder)) {
if (group) {
dispatch_group_enter(group);
}
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
......
这里先判断是否设置了SDWebImageDelayPlaceholder
这个选项,如果设置了则等到图片加载失败再设置占位图。
可以利用SDWebImageInternalSetImageGroupKey
属性设置dispatch_group_t
,应该要配合setImageBlock
来使用。因为看SD源码只有dispatch_group_enter
,没有dispatch_group_leave
,这个可能要我们自己处理。
- 初始化图片加载进度
......
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
......
- 获取SDWebImageManager
......
SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
......
可以通过SDWebImageExternalCustomManagerKey
设置自定义的SDWebImageManager
,不然就使用默认的。
- 设置图片加载进度回调
......
__weak __typeof(self)wself = self;
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);
}
};
......
这里把外部传进来的progressBlock
包了一层,先更新UIImageView
的sd_imageProgress
,再继续往下传。
- 创建
SDWebImageOperation
并保存到sd_setImageLoadOperation
......
id operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
......
到这里图片请求的创建就完成了。接下来看看加载图片的流程。
二、加载图片
上一步结束时调用了SDWebImageManager
的loadImageWithURL
方法来创建id
:
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
......
这个方法具体做了以下事情:
- 对
url
进行处理,防止传进来的是NSString
或NSNull
......
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
......
- 创建
SDWebImageCombinedOperation
......
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
......
SDWebImageCombinedOperation
这个类比较简单,只保存了下面这些属性:
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@property (weak, nonatomic, nullable) SDWebImageManager *manager;
downloadToken
和cacheOperation
会在下面的流程中创建,用弱引用保存了SDWebImageManager
是为了执行取消操作:
......
[self.manager safelyRemoveOperationFromRunning:self];
......
- 处理
failedUrl
failedURLs是NSMutableSet
,里面保存了失败过的URL。如果url的地址为空,或者该URL请求失败过且没有设置重试SDWebImageRetryFailed
选项,则直接直接调用完成。
......
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
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;
}
......
failedURLsLock
是dispatch_semaphore_t
,用来保证读写failedURLs的线程安全。
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
- 保存
SDWebImageCombinedOperation
,根据url生成cacheKey
......
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
NSString *key = [self cacheKeyForURL:url];
......
我们可以设置@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
来自定义cacheKey。
- 解析缓存策略参数
SDImageCacheQueryDataWhenInMemory
:默认当在内存找到结果后就不再到磁盘缓存查找,设置这个属性后强制到磁盘缓存查找。
SDImageCacheQueryDiskSync
:默认情况下内存缓存同步,磁盘缓存异步,这个属性可以强制磁盘缓存同步。
SDImageCacheScaleDownLargeImages
:默认不会对图片进行缩放,这个属性会根据设备的内存对图片进行适当的缩放。
......
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
......
接下来就是从缓存或网络加载图片。
2.1、从缓存加载
到SDImageCache
创建cacheOperation
。
- 先到内存缓存查找
SDMemoryCache
继承了NSCache
,里面其实还维护了一个weakCache,当收到内存警告时会全部清除缓存NSCache的内容,但是weakCache的不会清除,因为UIImageView还持有一些图片,这些图片还一直存在weakCache中。
磁盘清理策略:每次APP被杀死和进入后台都会进行一次清理。我们可以配置是根据上一次的修改时间还是访问时间来清理。这一步完成后如果缓存大小还是超过设置的最大值,那么还会继续删除,这时会对缓存的文件进行一次排序,删除最久的,知道整个缓存大小小于配置值。
如果没有设置SDImageCacheQueryDataWhenInMemory
属性,那找到图片后直接返回:
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
- 创建一个NSOperation,也就是返回的cacheOperation
它只是用来操控是否取消磁盘读取:
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
// 解析图片
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
最后根据SDImageCacheQueryDiskSync
来决定是异步执行还是同步执行。
- 从缓存加载完成
查询完缓存后继续,如果SDWebImageCombinedOperation
被回收或者取消则直接结束。
接下来判断是否需要下载:
- 没有设置只从缓存读取
SDWebImageFromCacheOnly
- cachedImage为空或者需要刷新缓存
SDWebImageRefreshCached
- 询问代理
......
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
......
2.2、从网络加载
新的版本可以通过SDWebImageFromLoaderOnly
来直接跳过查找缓存。
如果有缓存图片,只是刷新缓存的话,则先返回缓存的图片
......
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
......
- 获取下载配置项downloaderOptions
SDWebImageDownloaderLowPriority
:低优先级
SDWebImageDownloaderProgressiveDownload
:边加载变显示
SDWebImageDownloaderUseNSURLCache
:使用NSURLCache
SDWebImageDownloaderContinueInBackground
:App进入后台后继续下载图片
SDWebImageDownloaderHandleCookies
:处理NSHTTPCookieStore里面的Cookies
SDWebImageDownloaderAllowInvalidSSLCertificates
:忽略SSL证书
SDWebImageDownloaderHighPriority
:高优先级
SDWebImageDownloaderScaleDownLargeImages
:缩放图片
如果图片已缓存,只是刷新缓存,则取消SDWebImageDownloaderProgressiveDownload
且忽略NSURLCache
的缓存SDWebImageDownloaderIgnoreCachedResponse
。
调用SDWebImageDownloader
开始下载
......
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
......
这个方法做了这些事情:
a、获取downloadOperation
LOCK(self.operationsLock);
NSOperation *operation = [self.URLOperations objectForKey:url];
b、设置progressBlock、completedBlock
......
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
......
c、生成SDWebImageDownloadToken
......
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
......
2.2.1、imageDownloaderOperation
的创建
imageDownloaderOperation就是NSOperation
,和url一一对应,所以相同的url不会创建多个operation。如果在self.URLOperations
找不到就会开始创建新的:
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options];
createDownloaderOperationWithUrl:
- 创建
NSMutableURLRequest
NSTimeInterval timeoutInterval = self.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (self.headersFilter) {
request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [self allHTTPHeaderFields];
}
a、设置超时时间
b、设置缓存策略NSURLRequestCachePolicy
c、根据SDWebImageDownloaderHandleCookies
设置Cookie处理方式
d、设置HTTPShouldUsePipelining
为YES
e、设置头部
关于HTTPShouldUsePipelining
:
如果将HTTPShouldUsePipelining设置为YES, 则允许不必等到response, 就可以再次请求. 这个会很大的提高网络请求的效率,但是也可能会出问题.
因为客户端无法正确的匹配请求与响应, 所以这依赖于服务器必须保证,响应的顺序与客户端请求的顺序一致.如果服务器不能保证这一点, 那可能导致响应和请求混乱.
What are the disadvantage(s) of using HTTP pipelining?
HTTPShouldUsePipelining 解释
- 创建NSOperation
NSOperation *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
self.operationClass
默认是SDWebImageDownloaderOperation
,在SDWebImageDownloader
初始化时设置的:
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
operationClass
必须满足条件:
- (void)setOperationClass:(nullable Class)operationClass {
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperationInterface)]) {
_operationClass = operationClass;
} else {
_operationClass = [SDWebImageDownloaderOperation class];
}
}
- 设置证书
if (self.urlCredential) {
operation.credential = self.urlCredential;
} else if (self.username && self.password) {
operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
}
这里设置的证书会在NSURLSessionTaskDelegate
回调里面用到:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
- 设置优先级
......
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
......
- 设置顺序
......
if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
......
- operation创建完成并添加到队列
__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];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
[self.downloadQueue addOperation:operation];
downloadQueue
在SDWebImageDownloader
初始化时创建:
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
最大的并发线程为6
。
如果operation
已经存在且还未开始则修改优先级
else if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
} else {
operation.queuePriority = NSOperationQueuePriorityNormal;
}
}
UNLOCK(self.operationsLock);
如果operation已经在运行了,则什么也不做只添加进度回调和完成回调。
operation
用数组来保存progressBlock、completedBlock
:
- (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;
}
返回callbacks
,也就是传进来的progressBlock和completedBlock组成的字典。这个作为downloadOperationCancelToken
保存到SDWebImageDownloadToken
中,另外operation
也会保存到SDWebImageDownloadToken的downloadOperation
中,url保存到SDWebImageDownloadToken的url:
......
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
......
SDWebImageDownloadToken
保存在SDWebImageCombinedOperation
中:
......
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
......
downloadOperationCancelToken
在取消任务时有用,SDWebImageCombinedOperation的cancel:
- (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];
}
}
SDWebImage请求和加载图片的大致流程和相关变量的引用关系如下:
打破循环引用:
2.2.2、下载开始
下载开始的工作主要是在SDWebImageDownloaderOperation
的start
函数里面创建NSURLSessionTask
。
- 判断任务是否已被取消
首先用@synchronized
来加锁:
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
......
如果已被取消,则直接退出。
- 处理后台下载
判断当前系统是否有UIKit,如果有则向系统申请多一点时间:
#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:^{
[wself cancel];
}];
}
#endif
- 获取
NSURLSession
SDWebImageDownloaderOperation
有两个NSURLSession变量:
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
一个弱引用,一个强引用。弱引用的是从外面注入的,如果unownedSession
为空,则需要自己创建一个ownedSession
。但请求重置时需要我们自己处理这个ownedSession,让它失效并取消。
看看SDWebImageDownloaderOperation的初始化函数:
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
SDWebImageDownloader
在创建NSOperation
时传进这个unownedSession
,这个session在SDWebImageDownloader初始化创建:
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
[self cancelAllDownloads];
if (self.session) {
[self.session invalidateAndCancel];
}
sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
/**
* 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.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
先取消所有的下载任务,也就是移除downloadQueue
中的任务:
- (void)cancelAllDownloads {
[self.downloadQueue cancelAllOperations];
}
session在创建时把delegateQueue
设置为nil,这样session的回调会在并行队列中执行。另外delegate设置为SDWebImageDownloader
。
看看SDWebImageDownloader实现的协议
@interface SDWebImageDownloader ()
SDWebImageDownloader中这些协议方法的实现基本都是转发给NSOperation
处理。先根据NSURLSessionTask
找到对应的NSOperation
:
- (NSOperation *)operationWithTask:(NSURLSessionTask *)task {
NSOperation *returnOperation = nil;
for (NSOperation *operation in self.downloadQueue.operations) {
if ([operation respondsToSelector:@selector(dataTask)]) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
}
return returnOperation;
}
ownedSession
的delegate
自然是设置为SDWebImageDownloaderOperation
。
至此NSURLSession
已经拿到了。
- 处理
SDWebImageDownloaderIgnoreCachedResponse
这时如果设置了SDWebImageDownloaderIgnoreCachedResponse
,也就是不要返回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;
}
}
......
- 创建
NSURLSessionDataTask
最后就是创建NSURLSession了,并把当前状态设置为正在运行:
......
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
......
dataTask
开始后,就来到了NSURLSessionTaskDelegate, NSURLSessionDataDelegate
协议的方法,一共5个:
- didReceiveResponse
在这里拿到数据的总长度,并决定请求是否继续:
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
self.expectedSize = expected;
self.response = response;
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
BOOL valid = statusCode < 400;
//'304 Not Modified' is an exceptional one. It should be treated as cancelled if no cache data
//URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check
if (statusCode == 304 && !self.cachedData) {
valid = NO;
}
if (valid) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
} else {
// Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
disposition = NSURLSessionResponseCancel;
}
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf];
});
if (completionHandler) {
completionHandler(disposition);
}
}
- didReceiveData
这里拼接服务器返回的图片数据。
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];
加载进度的回调是在这里执行的:
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
另外如果设置了SDWebImageDownloaderProgressiveDownload
,就会边下载边显示图片,后面再说。
- willCacheResponse
如果设置了SDWebImageDownloaderUseNSURLCache
,那就不缓存数据。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSCachedURLResponse *cachedResponse = proposedResponse;
if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
// Prevents caching of responses
cachedResponse = nil;
}
if (completionHandler) {
completionHandler(cachedResponse);
}
}
- didReceiveChallenge
处理证书问题。收到NSURLAuthenticationMethodServerTrust
由我们来决定是否信任该服务器,如果设置了SDWebImageDownloaderAllowInvalidSSLCertificates
,则按默认处理。
其他情况可能是服务器要求我们提供认证,这时可以提供一些账号密码等。
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
} else {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
} else {
if (challenge.previousFailureCount == 0) {
if (self.credential) {
credential = self.credential;
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
The NSURLAuthenticationMethodServerTrust is not about you, the client, responding to an authentication challenge of the server but instead giving you, the client, the opportunity to check whether you should even trust the server at all. In that case the protectionSpace of the challenge contains a serverTrust object/struct that contains all the information needed to validate whether this server should be trusted. Generally, you would use the SecTrustEvaluate function to check the serverTrust and act according to the result of that check.
iOS中HTTP/HTTPS授权访问(一)
SURLAuthenticationMethodServerTrust is ssl
- didCompleteWithError
这个方法在加载完成后调用。
首先发出通知,此任务已经结束:
@synchronized(self) {
self.dataTask = nil;
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
}
});
}
这里不知道为什么要用__block包一遍
之后判断请求是否成功,成功后再判断CompletedCallback
不为空再进一步处理。
a、取出imageData
,若为空,则结束,返回错误;
b、如果imageData
和cachedData
一致,且设置了SDWebImageDownloaderIgnoreCachedResponse
,直接返回nil;
c、进入图片解码队列,缩放图片。
先将imageData解码为UIImage,然后调用内联函数SDScaledImageForKey(key, image)
设置Image的scale
。
这个方法主要是根据图片key的@2x和@3x来设置UIImage的scale
......
if (scale != image.scale) {
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
scaledImage.sd_imageFormat = image.sd_imageFormat;
image = scaledImage;
}
......
这个新的scale是如何确定的呢?是根据文件名来确定的:
......
CGFloat scale = 1;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
......
scale作为UIImage的一个属性,Apple是这样定义的:
If you load an image from a file whose name includes the @2x modifier, the scale is set to 2.0. You can also specify an explicit scale factor when initializing an image from a Core Graphics image. All other images are assumed to have a scale factor of 1.0.
If you multiply the logical size of the image (stored in the size property) by the value in this property, you get the dimensions of the image in pixels.
UIImage的像素宽高 = UIImage.size * UIImage.scale 。@2x图和@3x图的image size相等,但scale则不相等。
IOS:聊一聊UIImage几点知识
2.2.3、下载完成
NSOperation执行完成后调用callCompletionBlocksWithImage
方法,执行完成回调:
NSArray *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
}
});
完成回调是在SDWebImageDownloader
的downloadImageWithURL
方法中传进来的:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
......
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
......
这个方法又是在SDWebImageManager
的loadImageWithURL
方法中调用的,所以最后来到loadImageWithURL
中定义的回调:
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
......
- 先判断SDWebImageCombinedOperation是否为空或者已经被取消,如果是则什么都不做
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
}
- 请求失败,执行回调,判断下次是否直接阻止该地址的加载,如果是则加到failedURLs数组中
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);
}
- 请求成功
a、如果是SDWebImageRetryFailed
,先从failedURLs中移除。
......
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
......
b、判断是否缓存到磁盘
......
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
......
c、设置image的scale
......
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
......
这里要先判断SDWebImageManager不是默认的单例且cacheKeyFilter不为空。因为在SDWebImageDownloaderOperation是用[SDWebImageManager sharedManager]
来获取key的。
......
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
......
d、图片转换
如果不是Gif且实现了transformDownloadedImage
方法就会走到这里。
c、缓存图片
如果图片进行了转换那就只缓存转换后的图片。
我们可以设置cacheSerializer来自定义缓存的imageData。
d、调用完成回调
这时回到UIView + WebCache中定义的完成回调中。
2.2.4、完成回调
首先判断两个情况:是否调用完成回调、是否设置图片:
......
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
......
不设置图片的条件:
- image不为空但设置了SDWebImageAvoidAutoSetImage
- image为空且没有设置
SDWebImageDelayPlaceholder
调用完成回调的条件:
- 请求完成finished为YES
- finished为NO但设置了
SDWebImageAvoidAutoSetImage
只有设置了SDWebImageDownloaderProgressiveDownload时,从SDWebImageDownloaderOperation的didReceiveData里面调用完成回调时finished才是NO。
外部传进来的compledBlock被包了一层:
......
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
......
如果不设置图片,则直接调用上面的callCompletedBlockClojure。
如果图片为空且设置了SDWebImageDelayPlaceholder,则targetImage就是placeholder。
图片设置完成后,调用callCompletedBlockClojure
,整个流程就结束了。
三、图片设置
图片加载结束后调用下面这个方法进行图片的设置:
......
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
......
这里的setImageBlock
是一开始创建请求时从外部传进来的。
设置图片时首先创建一个最终的设置图片的block,根据UIView的类型来调用不用方法进行图片的设置:
......
UIView *view = self;
SDInternalSetImageBlock finalSetImageBlock;
if (setImageBlock) {
finalSetImageBlock = setImageBlock;
} else if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = (UIImageView *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
imageView.image = setImage;
};
}
#if SD_UIKIT
else if ([view isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
[button setImage:setImage forState:UIControlStateNormal];
};
}
#endif
......
transition
是SDWebImageTransition
,用来指定设置图片的动画。我们需要在UIView的sd_imageTransition
关联属性中设置。SDWebImage提供了一些默认的动画类型:
+ (nonnull instancetype)fadeTransition;
+ (nonnull instancetype)flipFromLeftTransition;
+ (nonnull instancetype)flipFromRightTransition;
+ (nonnull instancetype)flipFromTopTransition;
+ (nonnull instancetype)flipFromBottomTransition;
+ (nonnull instancetype)curlUpTransition;
+ (nonnull instancetype)curlDownTransition;
和UIView animateWithDuration:animations:
设置属性动画不同,UIView transitionWithView:duration:options:animations:completion:
用来做转场动画,主要是在容器中添加和移除View的动画。SDWebImageTransition
主要用来保存转场动画的options。
iOS UIView Animation - Transition
如果设置了动画,就在animations中设置图片:
......
[UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
finalSetImageBlock(image, imageData, cacheType, imageURL);
}
if (transition.animations) {
transition.animations(view, image);
}
} completion:transition.completion];
......
上面这段代码被一个转场动画包起来了:
[UIView transitionWithView:view duration:0 options:0 animations:^{
// 0 duration to let UIKit render placeholder and prepares block
if (transition.prepares) {
transition.prepares(view, image, imageData, cacheType, imageURL);
}
} completion:^(BOOL finished) {
......
}];
为何要这样?
- 隐式动画
- 理解CATransaction
- iOS CoreAnimation专题——原理篇(三) CALayer的模型层与展示层
- Core Animation Programming Guide
- 关于 Core Animation
- 计算机那些事(8)——图形图像渲染原理
- iOS 图像渲染原理