前言
SDWebImage源码阅读1——整体脉络结构
SDWebImage源码阅读2——缓存机制
前两篇研究了SDWebImage
的整体结构和缓存机制,本篇主要研究一下它的网络图片下载部分的代码。
分析
前面已经说过在SDWebImageManager
中有个SDWebImageDownloader
类型的imageDownloader
属性,意为** 下载管理器 **。当SDWebImageManager
在内存中查询图片不得时便开始了从网络下载图片,即调用了imageDownloader
的downloadImageWithURL:options:progress:completed:
方法。它返回了一个id <SDWebImageOperation>
类型的对象,并通过block回调下载的图片信息。
有关图片网络下载的东西都在这个下载管理器SDWebImageDownloader
内,我们现在开始仔细阅读它的代码:
—— SDWebImageDownloader.h——
// 下载的配置选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
// 这个属于默认的使用模式了,前往下载,返回进度block信息,完成时调用completedBlock
SDWebImageDownloaderLowPriority = 1 << 0,
// 渐进式下载 ,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 通常情况下request阻止使用NSURLCache.这个选项会默认使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果从NSURLCache中读取图片,会在调用完成block的时候,传递空的image或者imageData
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 进入后台,继续下载
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允许不受信任的SSL证书,在测试环境中很有用,在生产环境中要谨慎使用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 将图片下载放到高优先级队列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
// 下载顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// first in first out 先进先出
SDWebImageDownloaderFIFOExecutionOrder,
// last in first out
SDWebImageDownloaderLIFOExecutionOrder
};
// 定义通知的全局变量
extern NSString *const SDWebImageDownloadStartNotification;
extern NSString *const SDWebImageDownloadStopNotification;
// 定义回调的block
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
.h文件一开头定义了一些变量。有俩枚举变量,第一个是网络下载的配置选项,第二个是下载顺序。然后定义了两个全局性的常量,用于发送和观察下载开始和结束的通知。
值得注意的是在iOS中怎样去定义一个全局性的变量。标准方式就是这样,先在xxx.h文件中声明
extern NSString *const kCDKAPIHost;
,然后在xxx.m文件中声明NSString *const kCDKAPIHost = @"api.cheddarapp.com";
。除此之外,还可以通过定义setter/getter
的方式定义全局性变量。请参考:Objective-c怎么定义全局的静态变量
/**
* Asynchronous downloader dedicated and optimized for image loading.
*/
@interface SDWebImageDownloader : NSObject
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
// 当前下载队列最大的并发数,即队列中最多同时运行几条线程
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
// 超时时间
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
// 下载顺序
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
+ (SDWebImageDownloader *)sharedDownloader;
// 设置一个过滤器,为下载图片的HTTP request选取header.意味着最终使用的headers是经过这个block过滤之后的返回值。
@property (nonatomic, strong) NSDictionary *(^headersFilter)(NSURL *url, NSDictionary *headers);
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
// 是否暂停、挂起
- (void)setSuspended:(BOOL)suspended;
@end
可以看到它首先定义了一些暴露给外部、允许进行设置的、有关下载的属性,比如允许同时下载的最大线程数、当前线程数、超时时间、下载顺序、HTTP请求头等。然后给外部提供的接口最重要的就是downloadImageWithURL: options: completed:
了。
现在我们开始阅读SDWebImageDownloader.m
的代码。
——SDWebImageDownloader.m——
首先看下关于数据初始化部分的代码:
@interface SDWebImageDownloader ()
@property (strong, nonatomic) NSOperationQueue *downloadQueue; // 下载队列
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // URL回调字典,以URL为key,以该URL下载进度block和完成block的数组为value
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; // HTTP请求头
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
// barrierQueue是一个并行队列,在一个单一队列中顺序处理所有下载操作的网络响应
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
@end
@implementation SDWebImageDownloader
+ (void)initialize {
// Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator )
// To use it, just add #import "SDNetworkActivityIndicator.h" in addition to the SDWebImage import
if (NSClassFromString(@"SDNetworkActivityIndicator")) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#pragma clang diagnostic pop
// Remove observer in case it was previously added.
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"startActivity")
name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"stopActivity")
name:SDWebImageDownloadStopNotification object:nil];
}
}
+ (SDWebImageDownloader *)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (id)init {
if ((self = [super init])) {
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder; // 下载顺序(先进先出)
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 2;
_URLCallbacks = [NSMutableDictionary new];
_HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"]; // 初始化HTTP请求头
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); // 创建一个并行队列
_downloadTimeout = 15.0;
}
return self;
}
- (void)dealloc {
[self.downloadQueue cancelAllOperations]; // 移除下载队列中的所有操作
SDDispatchQueueRelease(_barrierQueue); // 销毁了自建的队列_barrierQueue
}
可以看到在类内部也定义了几个变量。有下载队列downloadQueue
,所有创建的下载操作operation
都会加入该队列管理;还有URL回调字典URLCallbacks
,以url为key,以下载进度回调progressBlock
和下载完成回调completedBlock
信息为value构建的字典。这个存有url回调信息字典,后面还会讲到;除此外,还定义了一个并行队列barrierQueue
,在该队列中做图片的网络响应处理,它在init中进行了初始化。
然后重写了initialize
方法,在内主要添加了网络开始下载和停止下载的观察。这个我有个疑问是为什么要重写initialize
方法呢?不可以在init
方法中完成吗?这个应该要清楚initialize
、init
与load
3个方法的区别与联系。
继续往下看代码。通过单例方法生成单例对象。然后在init
方法内对一些变量进行了初始化。最后在dealloc
方法中移除了下载队列中的所有操作,并销毁了自己创建的,用于图片网络响应处理的并行队列_barrierQueue
。
下面就是下载管理器里最重要的部分了:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(void (^)(NSInteger, NSInteger))progressBlock
completed:(void (^)(UIImage *, NSData *, NSError *, BOOL))completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak SDWebImageDownloader *wself = self;
// addProgressCallback:andCompletedBlock:forURL:createCallback:
// 这个方法的目的是在下载队列中为url开启下载操作,并且保证一条url只会被创建一次下载操作
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0){
timeoutInterval = 15.0;
}
// 创建请求对象request,并设置request的相关属性。
// 需要注意的是为避免重复缓存(NSURLCache + SDImageCache),若没有明确告知要进行URL请求缓存,则禁用NSURLCache缓存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES; // 是否开启HTTP管道,可以降低请求的加载时间
// 若自己通过headersFilter设置了请求头信息,则将其设为HTTP的请求头;否则,用在初始化方法中默认的HTTPHeaders作为HTTP请求头。
if (wself.headersFilter){
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else{
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// 创建SDWebImageDownLoaderOperation的operation下载操作对象
operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
if (!wself) return;
SDWebImageDownloader *sself = wself;
NSArray *callbacksForURL = [sself callbacksForURL:url];
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (!wself) return;
SDWebImageDownloader *sself = wself;
NSArray *callbacksForURL = [sself callbacksForURL:url];
if (finished) {
[sself removeCallbacksForURL:url];
}
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
if (!wself) return;
SDWebImageDownloader *sself = wself;
[sself removeCallbacksForURL:url];
}];
// 设置操作的优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
}
// 将下载操作放入下载队列中
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 如果是后进先出操作顺序 则将该操作置为最后一个操作
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
- (void)addProgressCallback:(void (^)(NSInteger, NSInteger))progressBlock andCompletedBlock:(void (^)(UIImage *, NSData *data, NSError *, BOOL))completedBlock forURL:(NSURL *)url createCallback:(void (^)())createCallback {
// URL作为callbacks字典的key,所以不能为空。若为空,则立即回调返回。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
// dispatch_barrier_sync“屏障”:保证同一时间只有一个线程操作URLCallbacks
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
// 处理 同一个URL的单个下载
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback(); // 通过这个回调,可以实时获取下载进度以及是下载完成情况
}
});
}
可以看到在方法downloadImageWithURL: options: progress: completed:
方法内部首先是调用了addProgressCallback: andCompletedBlock:forURL:createCallback:
这个方法,然后在其createCallback
回调block里进行了大量操作。这些操作主要是设置图片网络请求request
,创建下载操作operation
,并做回调的响应处理。关于addProgressCallback:
方法这块一直不太明白,搞不明白多出这个方法的目的,现在终于理解了。我们观察该方法内部:
它实际上是将某url对应下载操作的回调信息存储在了URLCallbacks
字典中。并且,若该url是第一次被下载,那字典URLCallbacks
中该url对应的value应该是空的,所以if(!self.URLCallbacks[url])
是YES,所以value被初始化,并将first
赋为YESfirst = YES
,后面完成该url对应value的赋值,也因为first==YES
,所以会执行createCallback()
,调用block回调。然后我们上面说了后续创建下载操作这些都是在该回调里完成的。那若该url不是第一次下载,它是不会调用createCallback()
回调的,也就是说根本不会执行到该block里写的创建下载操作的代码。简单的说,这儿的代码实现了防止了同一个url下载多次,只有第一次下载才会创建下载操作。
- (NSArray *)callbacksForURL:(NSURL *)url {
__block NSArray *callbacksForURL;
dispatch_sync(self.barrierQueue, ^{
callbacksForURL = self.URLCallbacks[url];
});
return [callbacksForURL copy];
}
- (void)removeCallbacksForURL:(NSURL *)url {
dispatch_barrier_async(self.barrierQueue, ^{
[self.URLCallbacks removeObjectForKey:url];
});
}
可以看到在创建下载操作的回调block中调用了以上两个方法,用于图片下载响应后的处理。第一个方法是从字典URLCallbacks
中读取出该url对应的回调信息。第二个方法是从URLCallbacks
中移除该url对应的回调信息。需要留意的是对该操作加了“屏障”,保证在进行移除这个动作时,只有一个线程存在。若在移除时存在多个线程,则是比较危险的,相反第一个方法纯粹读取字典信息时,即使是多线程也并不存在危险,所以没加“屏障”。
其实,进行网络连接,网络下载的核心是SDWebImageDownloaderOperation
类。在该类里通过对NSURLConnection
的封装完成了网络连接的管理,图片下载响应处理及优化等操作。这部分的具体实现就不写了。
结尾
图片的网络下载这部分很复杂,在这块儿磨了很久,总算捋出大致逻辑了。不过在阅读的过程中还是发现有很多需要补习的知识点。比如关于NSOperationQueue
,关于RunLoop
,关于HTTP
等都需要好好学习一下。
在阅读SDWebImage
源码的过程中发现网上有的版本和我的不太一致,我的应该不是最新的,所以我把我这个版本的代码也放上来:SDWebImage下载