前言
SDWebImage源碼閱讀1——整體脈絡(luò)結(jié)構(gòu)
SDWebImage源碼閱讀2——緩存機(jī)制
前兩篇研究了SDWebImage的整體結(jié)構(gòu)和緩存機(jī)制,本篇主要研究一下它的網(wǎng)絡(luò)圖片下載部分的代碼。
分析
前面已經(jīng)說(shuō)過(guò)在SDWebImageManager中有個(gè)SDWebImageDownloader類(lèi)型的imageDownloader屬性,意為** 下載管理器 **。當(dāng)SDWebImageManager在內(nèi)存中查詢(xún)圖片不得時(shí)便開(kāi)始了從網(wǎng)絡(luò)下載圖片,即調(diào)用了imageDownloader的downloadImageWithURL:options:progress:completed:方法。它返回了一個(gè)id <SDWebImageOperation>類(lèi)型的對(duì)象,并通過(guò)block回調(diào)下載的圖片信息。
有關(guān)圖片網(wǎng)絡(luò)下載的東西都在這個(gè)下載管理器SDWebImageDownloader內(nèi),我們現(xiàn)在開(kāi)始仔細(xì)閱讀它的代碼:
—— SDWebImageDownloader.h——
// 下載的配置選項(xiàng)
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
// 這個(gè)屬于默認(rèn)的使用模式了,前往下載,返回進(jìn)度block信息,完成時(shí)調(diào)用completedBlock
SDWebImageDownloaderLowPriority = 1 << 0,
// 漸進(jìn)式下載 ,如果設(shè)置了這個(gè)選項(xiàng),會(huì)在下載過(guò)程中,每次接收到一段返回?cái)?shù)據(jù)就會(huì)調(diào)用一次完成回調(diào),回調(diào)中的image參數(shù)為未下載完成的部分圖像,可以實(shí)現(xiàn)將圖片一點(diǎn)點(diǎn)顯示出來(lái)的功能
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 通常情況下request阻止使用NSURLCache.這個(gè)選項(xiàng)會(huì)默認(rèn)使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果從NSURLCache中讀取圖片,會(huì)在調(diào)用完成block的時(shí)候,傳遞空的image或者imageData
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 進(jìn)入后臺(tái),繼續(xù)下載
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 通過(guò)設(shè)置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來(lái)處理存儲(chǔ)在NSHTTPCookieStore的cookies
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允許不受信任的SSL證書(shū),在測(cè)試環(huán)境中很有用,在生產(chǎn)環(huán)境中要謹(jǐn)慎使用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 將圖片下載放到高優(yōu)先級(jí)隊(duì)列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
// 下載順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// first in first out 先進(jìn)先出
SDWebImageDownloaderFIFOExecutionOrder,
// last in first out
SDWebImageDownloaderLIFOExecutionOrder
};
// 定義通知的全局變量
extern NSString *const SDWebImageDownloadStartNotification;
extern NSString *const SDWebImageDownloadStopNotification;
// 定義回調(diào)的block
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
.h文件一開(kāi)頭定義了一些變量。有倆枚舉變量,第一個(gè)是網(wǎng)絡(luò)下載的配置選項(xiàng),第二個(gè)是下載順序。然后定義了兩個(gè)全局性的常量,用于發(fā)送和觀察下載開(kāi)始和結(jié)束的通知。
值得注意的是在iOS中怎樣去定義一個(gè)全局性的變量。標(biāo)準(zhǔn)方式就是這樣,先在xxx.h文件中聲明
extern NSString *const kCDKAPIHost;,然后在xxx.m文件中聲明NSString *const kCDKAPIHost = @"api.cheddarapp.com";。除此之外,還可以通過(guò)定義setter/getter的方式定義全局性變量。請(qǐng)參考:Objective-c怎么定義全局的靜態(tài)變量
/**
* Asynchronous downloader dedicated and optimized for image loading.
*/
@interface SDWebImageDownloader : NSObject
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
// 當(dāng)前下載隊(duì)列最大的并發(fā)數(shù),即隊(duì)列中最多同時(shí)運(yùn)行幾條線(xiàn)程
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
// 超時(shí)時(shí)間
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
// 下載順序
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
+ (SDWebImageDownloader *)sharedDownloader;
// 設(shè)置一個(gè)過(guò)濾器,為下載圖片的HTTP request選取header.意味著最終使用的headers是經(jīng)過(guò)這個(gè)block過(guò)濾之后的返回值。
@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
可以看到它首先定義了一些暴露給外部、允許進(jìn)行設(shè)置的、有關(guān)下載的屬性,比如允許同時(shí)下載的最大線(xiàn)程數(shù)、當(dāng)前線(xiàn)程數(shù)、超時(shí)時(shí)間、下載順序、HTTP請(qǐng)求頭等。然后給外部提供的接口最重要的就是downloadImageWithURL: options: completed:了。
現(xiàn)在我們開(kāi)始閱讀SDWebImageDownloader.m的代碼。
——SDWebImageDownloader.m——
首先看下關(guān)于數(shù)據(jù)初始化部分的代碼:
@interface SDWebImageDownloader ()
@property (strong, nonatomic) NSOperationQueue *downloadQueue; // 下載隊(duì)列
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // URL回調(diào)字典,以URL為key,以該URL下載進(jìn)度block和完成block的數(shù)組為value
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; // HTTP請(qǐng)求頭
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
// barrierQueue是一個(gè)并行隊(duì)列,在一個(gè)單一隊(duì)列中順序處理所有下載操作的網(wǎng)絡(luò)響應(yīng)
@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; // 下載順序(先進(jìn)先出)
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 2;
_URLCallbacks = [NSMutableDictionary new];
_HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"]; // 初始化HTTP請(qǐng)求頭
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); // 創(chuàng)建一個(gè)并行隊(duì)列
_downloadTimeout = 15.0;
}
return self;
}
- (void)dealloc {
[self.downloadQueue cancelAllOperations]; // 移除下載隊(duì)列中的所有操作
SDDispatchQueueRelease(_barrierQueue); // 銷(xiāo)毀了自建的隊(duì)列_barrierQueue
}
可以看到在類(lèi)內(nèi)部也定義了幾個(gè)變量。有下載隊(duì)列downloadQueue,所有創(chuàng)建的下載操作operation都會(huì)加入該隊(duì)列管理;還有URL回調(diào)字典URLCallbacks,以u(píng)rl為key,以下載進(jìn)度回調(diào)progressBlock和下載完成回調(diào)completedBlock信息為value構(gòu)建的字典。這個(gè)存有url回調(diào)信息字典,后面還會(huì)講到;除此外,還定義了一個(gè)并行隊(duì)列barrierQueue,在該隊(duì)列中做圖片的網(wǎng)絡(luò)響應(yīng)處理,它在init中進(jìn)行了初始化。
然后重寫(xiě)了initialize方法,在內(nèi)主要添加了網(wǎng)絡(luò)開(kāi)始下載和停止下載的觀察。這個(gè)我有個(gè)疑問(wèn)是為什么要重寫(xiě)initialize方法呢?不可以在init方法中完成嗎?這個(gè)應(yīng)該要清楚initialize、init與load3個(gè)方法的區(qū)別與聯(lián)系。
繼續(xù)往下看代碼。通過(guò)單例方法生成單例對(duì)象。然后在init方法內(nèi)對(duì)一些變量進(jìn)行了初始化。最后在dealloc方法中移除了下載隊(duì)列中的所有操作,并銷(xiāo)毀了自己創(chuàng)建的,用于圖片網(wǎng)絡(luò)響應(yīng)處理的并行隊(duì)列_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:
// 這個(gè)方法的目的是在下載隊(duì)列中為url開(kāi)啟下載操作,并且保證一條url只會(huì)被創(chuàng)建一次下載操作
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0){
timeoutInterval = 15.0;
}
// 創(chuàng)建請(qǐng)求對(duì)象request,并設(shè)置request的相關(guān)屬性。
// 需要注意的是為避免重復(fù)緩存(NSURLCache + SDImageCache),若沒(méi)有明確告知要進(jìn)行URL請(qǐng)求緩存,則禁用NSURLCache緩存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES; // 是否開(kāi)啟HTTP管道,可以降低請(qǐng)求的加載時(shí)間
// 若自己通過(guò)headersFilter設(shè)置了請(qǐng)求頭信息,則將其設(shè)為HTTP的請(qǐng)求頭;否則,用在初始化方法中默認(rèn)的HTTPHeaders作為HTTP請(qǐng)求頭。
if (wself.headersFilter){
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else{
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// 創(chuàng)建SDWebImageDownLoaderOperation的operation下載操作對(duì)象
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];
}];
// 設(shè)置操作的優(yōu)先級(jí)
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
}
// 將下載操作放入下載隊(duì)列中
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 如果是后進(jìn)先出操作順序 則將該操作置為最后一個(gè)操作
[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,所以不能為空。若為空,則立即回調(diào)返回。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
// dispatch_barrier_sync“屏障”:保證同一時(shí)間只有一個(gè)線(xiàn)程操作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
// 處理 同一個(gè)URL的單個(gè)下載
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(); // 通過(guò)這個(gè)回調(diào),可以實(shí)時(shí)獲取下載進(jìn)度以及是下載完成情況
}
});
}
可以看到在方法downloadImageWithURL: options: progress: completed:方法內(nèi)部首先是調(diào)用了addProgressCallback: andCompletedBlock:forURL:createCallback:這個(gè)方法,然后在其createCallback回調(diào)block里進(jìn)行了大量操作。這些操作主要是設(shè)置圖片網(wǎng)絡(luò)請(qǐng)求request,創(chuàng)建下載操作operation,并做回調(diào)的響應(yīng)處理。關(guān)于addProgressCallback:方法這塊一直不太明白,搞不明白多出這個(gè)方法的目的,現(xiàn)在終于理解了。我們觀察該方法內(nèi)部:
它實(shí)際上是將某url對(duì)應(yīng)下載操作的回調(diào)信息存儲(chǔ)在了URLCallbacks字典中。并且,若該url是第一次被下載,那字典URLCallbacks中該url對(duì)應(yīng)的value應(yīng)該是空的,所以if(!self.URLCallbacks[url])是YES,所以value被初始化,并將first賦為YESfirst = YES,后面完成該url對(duì)應(yīng)value的賦值,也因?yàn)?code>first==YES,所以會(huì)執(zhí)行createCallback(),調(diào)用block回調(diào)。然后我們上面說(shuō)了后續(xù)創(chuàng)建下載操作這些都是在該回調(diào)里完成的。那若該url不是第一次下載,它是不會(huì)調(diào)用createCallback()回調(diào)的,也就是說(shuō)根本不會(huì)執(zhí)行到該block里寫(xiě)的創(chuàng)建下載操作的代碼。簡(jiǎn)單的說(shuō),這兒的代碼實(shí)現(xiàn)了防止了同一個(gè)url下載多次,只有第一次下載才會(huì)創(chuàng)建下載操作。
- (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];
});
}
可以看到在創(chuàng)建下載操作的回調(diào)block中調(diào)用了以上兩個(gè)方法,用于圖片下載響應(yīng)后的處理。第一個(gè)方法是從字典URLCallbacks中讀取出該url對(duì)應(yīng)的回調(diào)信息。第二個(gè)方法是從URLCallbacks中移除該url對(duì)應(yīng)的回調(diào)信息。需要留意的是對(duì)該操作加了“屏障”,保證在進(jìn)行移除這個(gè)動(dòng)作時(shí),只有一個(gè)線(xiàn)程存在。若在移除時(shí)存在多個(gè)線(xiàn)程,則是比較危險(xiǎn)的,相反第一個(gè)方法純粹讀取字典信息時(shí),即使是多線(xiàn)程也并不存在危險(xiǎn),所以沒(méi)加“屏障”。
其實(shí),進(jìn)行網(wǎng)絡(luò)連接,網(wǎng)絡(luò)下載的核心是SDWebImageDownloaderOperation類(lèi)。在該類(lèi)里通過(guò)對(duì)NSURLConnection的封裝完成了網(wǎng)絡(luò)連接的管理,圖片下載響應(yīng)處理及優(yōu)化等操作。這部分的具體實(shí)現(xiàn)就不寫(xiě)了。
結(jié)尾
圖片的網(wǎng)絡(luò)下載這部分很復(fù)雜,在這塊兒磨了很久,總算捋出大致邏輯了。不過(guò)在閱讀的過(guò)程中還是發(fā)現(xiàn)有很多需要補(bǔ)習(xí)的知識(shí)點(diǎn)。比如關(guān)于NSOperationQueue,關(guān)于RunLoop,關(guān)于HTTP等都需要好好學(xué)習(xí)一下。
在閱讀SDWebImage源碼的過(guò)程中發(fā)現(xiàn)網(wǎng)上有的版本和我的不太一致,我的應(yīng)該不是最新的,所以我把我這個(gè)版本的代碼也放上來(lái):SDWebImage下載