SDWebImage是一个OC常用的图片下载第三方框架,其负责下载的类主要有SDWebImageDownloader和SDWebImageDownloaderOperation,下面来分析一下这两个类。本文分析的版本为4.4.3
SDWebImageDownloaderOperation
SDWebImageDownloaderOperation是一个NSOperation的子类的任务是把一张图片从服务器下载到本地。先来看一下其.h文件:
SDWebImageDownloaderOperationInterface
在其h文件的开头,定义了一个SDWebImageDownloaderOperationInterface的协议,SDWebImageDownloaderOperation遵循这个协议,如果用户想自定义一个下载任务,只需遵循这个协议,就可以被SDWebImageDownloader调度了,下面来看看协议的接口
@protocol SDWebImageDownloaderOperationInterface
@required
//初始化方法,显然一个请求对应一个任务
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options;
//给任务添加进度和完成的回调
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
//是否应该解压图片,下面两个方法可以用一个属性去遵循,
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
//是否需要设置凭证,用于HTTPS请求,下面两个方法可以用一个属性去遵循
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
//取消一系列的回调,token是这一些列回调的表示
- (BOOL)cancel:(nullable id)token;
@optional
//下载任务
- (nullable NSURLSessionTask *)dataTask;
@end
SDWebImageDownloaderOperation所暴露的接口基本也是这些。需要注意的事,如果遵循了这个协议,只需要在.m文件实现即可,这里为了让开发者使用时更加直观,再次将这些方法写到.h文件里。下面来看一下它的实现部分。
SDWebImageDownloaderOperation.m
基本方法
首先是它的初始化方法:
- (nonnull instancetype)init {
return [self initWithRequest:nil inSession:nil options:0];
}
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
if ((self = [super init])) {
_request = [request copy];
_shouldDecompressImages = YES;
//下载的策略
_options = options;
//一个保存回调的数组,其成员变量是一个字典
//这个字典的键只有两种情况,kProgressCallbackKey和kCompletedCallbackKey,分别代表进度回调和完成回调
//值为回调的block
_callbackBlocks = [NSMutableArray new];
这里重写了父类的两个属性并进行手动管理,原因后面讲
_executing = NO;
_finished = NO;
下载文件的大小
_expectedSize = 0;
//这里弱引用了传入的session
_unownedSession = session;
//管理回调数组的锁,是一个信号量
_callbacksLock = dispatch_semaphore_create(1);
//创建队列
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
然后是给任务添加block的代码
- (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;
}
其中LOCK的宏如下:
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
显然这是一个信号量的锁,是iOS中最高效的锁
- (nullable NSArray *)callbacksForKey:(NSString *)key {
LOCK(self.callbacksLock);
NSMutableArray *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
UNLOCK(self.callbacksLock);
[callbacks removeObjectIdenticalTo:[NSNull null]];
return [callbacks copy]; // strip mutability here
}
数组调用valueForKey方法会使得其中所有元素都调用valueForKey方法再生成一个数组,如果其中有元素的没有这个key,则返回的数组包含[NSNull null]],因此在生成的结果要移除掉这些空值
- (BOOL)cancel:(nullable id)token {
BOOL shouldCancel = NO;
LOCK(self.callbacksLock);
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
UNLOCK(self.callbacksLock);
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
取消某一回调,如果所有的回调都被取消,则取消该任务。
start方法
SDWebImageDownloaderOperation选择重写start()方法来执行其中的任务,代码如下:
//0.start方法在队列在该任务放入队列就会执行
- (void)start {
@synchronized (self) {
//1.如果任务已经取消,结束该任务
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//2.根据配置项向app申请后台运行时间,使得app退到后台时仍能执行任务
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;
}
}];
}
//3.配置好session,如果传入session为nil,则让自己成为session的代理
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
//4.根据选项读取url缓存中的数据
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
//5.开启下载任务,并通知外界
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
[self.dataTask resume];
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:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
6.取消后台任务,这个地方没看懂
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;
}
}
0.这个地方使用重写start方法而非main方法,其两者的区别在于当main方法执行完后任务的finishd属性就被置为YES,队列就会根据这个属性将该移除,而使用start方法则不会,这里使用start方法的原因是该类还可能需要处理请求的回调(当传入的session为nil时),这个回调可能会在这片代码执行之后才会被调用,也就是说,这个任务在请求回调后才算结束,因此需要手动管理finishd,除了这个属性外,executing也是手动管理的。
1.外界调用cancel并不会真正地取消正在执行的任务,只会把它的isCancelled属性置为YES
2.如果配置了后台下载,则要向应用申请时间,一般能申请180秒,申请时间到后需要调用endBackgroundTask方法,否则会crash
6.这个地方没明白,在提交记录上看到有这么一段话:
SDWebImageDownloaderOperation: call endBackgroundTask after operation is finished so that beginBackgroundTaskWithExpirationHandler and endBackgroundTask are balanced
意思是在任务结束的时候调用endBackgroundTask,但根据0的分析,start结束后并不代表任务结束,因此我并不认为这段代码放在这个地方是合理的
内部还有几个方法,用于管理任务的下载流程
//取消任务
- (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.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
结束任务
- (void)done {
self.finished = YES;
self.executing = NO;
[self reset];
}
//重置任务
- (void)reset {
LOCK(self.callbacksLock);
[self.callbackBlocks removeAllObjects];
UNLOCK(self.callbacksLock);
self.dataTask = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
//重写finished、executing的set方法,使他们向父类一样可被kvo监听
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
上面这些方法没什么好说的,值得一提的是,重写的是start方法而不是main方法,因此需要手动管理cancel、finish、excecuting等变量
网络协议相关部分
从上面的start方法可知,如果在初始化时传入的session为nil,则SDWebImageDownloaderOperation会创建一个session并让自己作为改session的代理,而如果使用了SDWebImageDownloader来下载图片,网络的请求的回调也会调到这里来。
//收到响应头的回调
- (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(本地重定向),但本地又没有缓存,也是这个请求无效的
if (statusCode == 304 && !self.cachedData) {
valid = NO;
}
if (valid) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
} else {
//标记为取消,如果直接取消,可能会延长session的生命周期
disposition = NSURLSessionResponseCancel;
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
});
//调用这个block表示是否允许这次下载
if (completionHandler) {
completionHandler(disposition);
}
}
这个回调的处理逻辑比较简单,也是没什么好说的
//收到响应数据的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];
//SDWebImageDownloaderProgressiveDownload这个选项表示是否阶段性展示图片
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
__block NSData *imageData = [self.imageData copy];
const NSInteger totalSize = imageData.length;
BOOL finished = (totalSize >= self.expectedSize);
//初始化序列化器
if (!self.progressiveCoder) {
// We need to create a new instance for progressive decoding to avoid conflicts
for (idcoder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id)coder) canIncrementallyDecodeFromData:imageData]) {
self.progressiveCoder = [[[coder class] alloc] init];
break;
}
}
}
// 在另外的线程对图片解码
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
// 在completionBlock里面回调,这个时候由于还没下载完成,finished为NO
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
}
//进度回调
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
如果把中间的条件代码去掉,这个回调还是很容易看得懂的。
//是否缓存响应
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSCachedURLResponse *cachedResponse = proposedResponse;
//如果没有缓存响应策略
if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
//防止缓存
cachedResponse = nil;
}
if (completionHandler) {
//传入的数据即为缓存的数据
completionHandler(cachedResponse);
}
}
这个策略默认是不打开的,也就是不缓存,如果把回调的参数设置为nil,就不会缓存响应,因为真正缓存的数据就是回调中的参数。
//响应完成的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
//在主线程发送响应的通知
@synchronized(self) {
self.dataTask = nil;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
}
});
}
//不管是哪一个条件分支,最终都要调用 [self done]表示任务结束
if (error) {
[self callCompletionBlocksWithError:error];
[self done];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
__block NSData *imageData = [self.imageData copy];
if (imageData) {
//这个选项不会将图片image回调出去
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
[self done];
} else {
//解压图片
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
BOOL shouldDecode = YES;
// Do not force decoding animated GIFs and WebPs
if (image.images) {
shouldDecode = NO;
} else {
#ifdef SD_WEBP
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
if (imageFormat == SDImageFormatWebP) {
shouldDecode = NO;
}
#endif
}
//按照需要解码图片
if (shouldDecode) {
if (self.shouldDecompressImages) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
[self done];
}
} else {
[self done];
}
}
}
这块代码主要复杂在对完成数据的处理,包括数据转图片、图片解码等等,这一块就涉及到另外的知识点了,这里不再赘述
//需要证书验证的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
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);
}
}
这里如果请求时https的,则需要用到这个方法来处理
SDWebImageDownloader
SDWebImageDownloaderOperation主要是负责下载单个图片的类,而在开发中,图片的下载任务往往是成批出现的,因此需要一个管理这些下载任务的类,SDWebImageDownloader就是这样的类。
SDWebImageDownloader.h
下面来看一下SDWebImageDownloader.h文件。
SDWebImageDownloaderOptions
SDWebImageDownloaderOptions是下载的选项,如下
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
//下载低优先级
SDWebImageDownloaderLowPriority = 1 << 0,
//在下载过程中展示部分下载的图片
SDWebImageDownloaderProgressiveDownload = 1 << 1,
//使用NSURLCache缓存响应
SDWebImageDownloaderUseNSURLCache = 1 << 2,
//忽略缓存的响应
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
//后台下载
SDWebImageDownloaderContinueInBackground = 1 << 4,
//使用cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
//允许无效证书验证
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
//下载高优先级
SDWebImageDownloaderHighPriority = 1 << 7,
//裁剪图片
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
这里设计了几种不同的选项,开发者可以根据自己的需求添加合适的选项。如果想多个选项一起使用,使用|即可。
SDWebImageDownloaderExecutionOrder
这是一个下载任务的调用顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
//先进先出
SDWebImageDownloaderFIFOExecutionOrder,
//后进先出
SDWebImageDownloaderLIFOExecutionOrder
};
SDWebImageDownloadToken
SDWebImageDownloadToken是每一个下载的唯一身份标识,SDWebImageOperation是一个协议,它还有cancel这个方法
@interface SDWebImageDownloadToken : NSObject
//url
@property (nonatomic, strong, nullable) NSURL *url;
//取消的回调
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
@end
SDWebImagerDownloader
@interface SDWebImageDownloader : NSObject
//是否压缩图片,默认是YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
//最大并发数,默认是6
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
//并发数
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
//下载超时时间,默认是15秒
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
//URLSession,默认是全局的session
@property (readonly, nonatomic, nonnull) NSURLSessionConfiguration *sessionConfiguration;
//下载策略,默认是先进后出
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
//获得单例,这个单例并不是严格的
+ (nonnull instancetype)sharedDownloader;
//凭证配置
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;
//用户名,配置证书用
@property (strong, nonatomic, nullable) NSString *username;
//密码,配置证书用
@property (strong, nonatomic, nullable) NSString *password;
//请求头过滤的block
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;
//初始化方法
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
//设置请求头
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
//拿到请求头的配置
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;
//设置下载任务的类,默认是SDWebImageDownloaderOperation,传入的类必须是NSOperation的子类且遵循
//SDWebImageDownloaderOperationInterface协议才生效
- (void)setOperationClass:(nullable Class)operationClass;
//下载任务的主要方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
//取消任务
- (void)cancel:(nullable SDWebImageDownloadToken *)token;
//挂起任务
- (void)setSuspended:(BOOL)suspended;
取消所有任务
- (void)cancelAllDownloads;
//配置新的session
- (void)createNewSessionWithConfiguration:(nonnull NSURLSessionConfiguration *)sessionConfiguration;
//取消session,并取消下载
- (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations;
@end
SDWebImagerDownloader对外暴露的接口有点多,大致可以分为初始化,配置,下载,任务管理这几个部分,其主要的下载接口只有一个。
下面我们来看一下其.m文件的实现
SDWebImageDownloadToken
@interface SDWebImageDownloadToken ()
@property (nonatomic, weak, nullable) NSOperation *downloadOperation;
@end
@implementation SDWebImageDownloadToken
- (void)cancel {
if (self.downloadOperation) {
SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
if (cancelToken) {
[self.downloadOperation cancel:cancelToken];
}
}
}
@end
首先是SDWebImageDownloadToken的cancel的实现,这是SDWebImageOperation的一个方法,在这里取消其下载的任务
初始化部分
//返回一个全局的实例
+ (nonnull instancetype)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (nonnull instancetype)init {
return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
//在这里进行完整的初始化
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];//配置默认的下载任务的类
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;//配置下载策略
_downloadQueue = [NSOperationQueue new];//创建下载队列
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];//管理下载任务
SDHTTPHeadersMutableDictionary *headerDictionary = [SDHTTPHeadersMutableDictionary dictionary];
NSString *userAgent = nil;
#if SD_UIKIT
//配置请求头的默认userAgent
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#endif
if (userAgent) {
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
headerDictionary[@"User-Agent"] = userAgent;
}
#ifdef SD_WEBP
//配置响应头的Accept
headerDictionary[@"Accept"] = @"image/webp,image/*;q=0.8";
#else
headerDictionary[@"Accept"] = @"image/*;q=0.8";
#endif
//缓存响应头
_HTTPHeaders = headerDictionary;
//两把锁
_operationsLock = dispatch_semaphore_create(1);
_headersLock = dispatch_semaphore_create(1);
//下载超时时间
_downloadTimeout = 15.0;
//配置session
[self createNewSessionWithConfiguration:sessionConfiguration];
}
return self;
}
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
[self cancelAllDownloads];
//先取消原来的session
if (self.session) {
[self.session invalidateAndCancel];
}
sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
//这里并没有使用sharedsession得到全局的session
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
可以看出,这并不是一个完整的单例,如果使用sharedDownloader,则总会返回同一个Downloader,使用init或initWithSessionConfiguration创建则返回不同的Downloader。
createNewSessionWithConfiguration这个方法是一个对外暴露的接口,因此在配置前,需要先把原来的session取消掉,再配置新的session。
此外,既然session创建出来,那就得在Downloader释放后将其取消掉:
- (void)dealloc {
[self.session invalidateAndCancel];
self.session = nil;
[self.downloadQueue cancelAllOperations];
}
此外,Downloader还i提供nvalidateSessionAndCancel是个外部可调用的借口。也就是说,取消的时机可由开发者控制
- (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations {
//如果是全局的,则无法取消掉
if (self == [SDWebImageDownloader sharedDownloader]) {
return;
}
if (cancelPendingOperations) {
//直接取消session,包括正在执行的任务
[self.session invalidateAndCancel];
} else {
//取消session,但正在执行的任务会执行完成,之后的任务会被取消掉
[self.session finishTasksAndInvalidate];
}
}
配置部分
//配置请求头
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field {
LOCK(self.headersLock);
if (value) {
self.HTTPHeaders[field] = value;
} else {
[self.HTTPHeaders removeObjectForKey:field];
}
UNLOCK(self.headersLock);
}
//根据field获取相应的请求头信息
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {
if (!field) {
return nil;
}
return [[self allHTTPHeaderFields] objectForKey:field];
}
//获取所有的请求头信息
- (nonnull SDHTTPHeadersDictionary *)allHTTPHeaderFields {
LOCK(self.headersLock);
SDHTTPHeadersDictionary *allHTTPHeaderFields = [self.HTTPHeaders copy];
UNLOCK(self.headersLock);
return allHTTPHeaderFields;
}
//设置队列的最大并发数
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {
_downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
//获取现在队列的并发数
- (NSUInteger)currentDownloadCount {
return _downloadQueue.operationCount;
}
//获取队列的最大并发数
- (NSInteger)maxConcurrentDownloads {
return _downloadQueue.maxConcurrentOperationCount;
}
//配置session的配置
- (NSURLSessionConfiguration *)sessionConfiguration {
return self.session.configuration;
}
//设置下载任务的类
- (void)setOperationClass:(nullable Class)operationClass {
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperationInterface)]) {
_operationClass = operationClass;
} else {
_operationClass = [SDWebImageDownloaderOperation class];
}
}
这一部分都是暴露给外界的接口,实现也比较简单。
下载部分
首先是下载的核心方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
//如果url为空,则返回
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
LOCK(self.operationsLock);
//按照url取出对应的任务(如果有)
NSOperation *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished) {
//如果任务已完成或者任务已经结束,则创建一个任务
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
//任务结束后通知Downloader将自己移除
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
//加入缓存,并开始改任务
[self.URLOperations setObject:operation forKey:url];
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//下面的代码是为任务创建一个标识,
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
这个方法给外界返回一个Token,外界可通过这个Token将任务取消掉
opeation创建的代码如下:
- (NSOperation *)createDownloaderOperationWithUrl:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options {
//设置超时时间
NSTimeInterval timeoutInterval = self.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//根据选项决定请求是否缓存
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
//根据选项决定是否使用cookies
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
//设置请求头,如果有过滤的函数,则调用
if (self.headersFilter) {
request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [self allHTTPHeaderFields];
}
//创建下载任务,这个任务是由operationClass创建的,而默认是SDWebImageDownloaderOperation
NSOperation *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
operation.shouldDecompressImages = self.shouldDecompressImages;
//配置凭证
if (self.urlCredential) {
operation.credential = self.urlCredential;
} else if (self.username && self.password) {
operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
}
//配置高/低优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//如果设置成后进先出,则添加任务的依赖关系以实现
if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
return operation;
}
在最后可以看出其后进先出的实现方式,这里我觉得是有些问题的:
- 从下载的代码可以看出,任务一旦创建马上就添加到队列里面,一旦开始执行了,就算存在新的任务,并为原来的任务添加依赖,那还是会继续执行的,新来的任务该等还是得等,并不能实现后进先出的效果
- 这个下载队列默认有6条并发线程,但是一旦这种依赖关系建立起来,在下载任务繁多的时候,任务就只能一个接一个地执行了,相当于一条串行队列
任务管理部分
//按照token取消下载
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
NSURL *url = token.url;
if (!url) {
return;
}
LOCK(self.operationsLock);
NSOperation *operation = [self.URLOperations objectForKey:url];
if (operation) {
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:url];
}
}
UNLOCK(self.operationsLock);
}
//队列挂起
- (void)setSuspended:(BOOL)suspended {
self.downloadQueue.suspended = suspended;
}
//取消所有任务
- (void)cancelAllDownloads {
[self.downloadQueue cancelAllOperations];
}
这部分也是比较简单,不在赘述
URLSession协议部分
SDWebImageDownloader也遵循了URLSession的代理协议,但是,在协议的回调中,交给对应的operation去执行
//通过task获取对应的operation
- (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;
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSOperation *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSOperation *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSOperation *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
[dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(proposedResponse);
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSOperation *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
[dataOperation URLSession:session task:task didCompleteWithError:error];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
NSOperation *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
[dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(request);
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSOperation *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
[dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
}
可以看出,上面的实现都一样的,基本都是先看看有没有对应的operation,没有就走默认的回调方法。
心得
原本以为只是下载部分的代码会简单一些,没想到居然花了2周来看,在这期间补了好多知识点,虽然还有一些我没有看懂的地方,但这种下载图片的思想和多线程的使用还是值得我们去学习的。有空再来补一下它的其它部分吧。