一. 下載器的介紹
下載器在SDWebImage中和緩存是相輔相成的(關(guān)于它們的合作要在 <SDWebImage ─── 總結(jié)>才會說明)另假。下載器(其實用下載操作生成器來形容比較貼切)提供這樣一個功能:根據(jù)提供的參數(shù)生成一個下載操作件缸,把下載操作返回給你,并且下載中或完成時會通過Block回調(diào)給你趾牧。簡單點說,你給我一個url,我創(chuàng)建一個操作去下載拇泣。
SDWebImage的下載器功能主要有兩個類組成。
SDWebImageDownloader矮锈;//管理類霉翔,管理所有的下載操作
SDWebImageDownloaderOperation;//繼承NSOperation的操作類苞笨,主要用來下載
通過該篇下載器的學(xué)習(xí)债朵,你可以學(xué)到如何設(shè)計一個能同時進行多個下載操作的下載器,也可以更加了解多線程和網(wǎng)絡(luò)這塊的知識點瀑凝⌒蚵可能之前都是用AFNetWorking實現(xiàn)下載功能,而忽視了最基礎(chǔ)的NSURLSession粤咪。
二. 下載器的設(shè)計
說起下載器的設(shè)計谚中,我們主要從三個方面來說:
① 下載操作管理類的設(shè)計(SDWebImageDownloader)
② 下載操作類的設(shè)計(SDWebImageDownloaderOperation)
這兩個的職責(zé)其實很明確,我們知道SDWebImageDownloaderOperation是繼承NSOperation類寥枝,而它主要是封裝了下載的處理的內(nèi)容宪塔,才會被稱為下載操作類。而SDWebImageDownloader是用來管理這些下載操作類的囊拜,因為多個下載時也會有多個下載操作某筐,所以這邊要由下載操作管理類來統(tǒng)一管理。
① 下載操作管理類的設(shè)計
要談到下載操作管理類的設(shè)計冠跷,我們就要先清楚管理類作用就是管理所有的下載操作南誊,讓多個下載操作能各司其職,互不干擾蔽莱。同時弟疆,能夠設(shè)置一些下載所需要的網(wǎng)絡(luò)屬性,比如請求頭盗冷,會話等等。
我們通過SDWebImageDownloader暴露的屬性和API就可以發(fā)現(xiàn)都是圍繞著下載網(wǎng)絡(luò)相關(guān)設(shè)置和下載操作管理這兩點來進行的(當(dāng)然同廉,本質(zhì)都是操作SDWebImageDownloader內(nèi)部的私有屬性,比如downloadQueue仪糖、HTTPHeaders、session)迫肖。
- 用來設(shè)置自定義證書
- 用來設(shè)置請求頭
- 設(shè)置隊列屬性(最大并行下載數(shù)量锅劝,超時時間,下載順序)
- 用來管理下載操作(下載蟆湖,暫停故爵,取消)
/* 用來設(shè)置下載后要不要立即解壓圖片 */
@property (assign, nonatomic) BOOL shouldDecompressImages;
/* 用來設(shè)置隊列屬性 */
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
/* 用來設(shè)置自定義證書 */
@property (strong, nonatomic) NSURLCredential *urlCredential;
@property (strong, nonatomic) NSString *username;
@property (strong, nonatomic) NSString *password;
/* 用來設(shè)置請求頭 */
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
/* 用來管理下載操作 */
- (void)setOperationClass:(Class)operationClass;
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (void)setSuspended:(BOOL)suspended;
- (void)cancelAllDownloads;
其中我們主要用到下載的方法,也是下載管理類的核心API:給我提供對應(yīng)的參數(shù)隅津,給你創(chuàng)建一個下載操作诬垂,添加到我的下載隊列中劲室。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
我們可以看出創(chuàng)建一個下載隊列需要這么幾個參數(shù):
- url(下載地址)
- options(下載選項,如何去下載以及下載后怎么做)
- progressBlock(下載過程中的回調(diào))
- completedBlock(下載完成后的回調(diào))
其中options給我們提供了很多類型的模式:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
//這個屬于默認的使用模式了,前往下載,返回進度block信息,完成時調(diào)用completedBlock
SDWebImageDownloaderLowPriority = 1 << 0,
//漸進式下載 ,如果設(shè)置了這個選項,會在下載過程中,每次接收到一段返回數(shù)據(jù)就會調(diào)用一次完成回調(diào),回調(diào)中的image參數(shù)為未下載完成的部分圖像,可以實現(xiàn)將圖片一點點顯示出來的功能
SDWebImageDownloaderProgressiveDownload = 1 << 1,
//使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
//如果從NSURLCache中讀取圖片,會在調(diào)用完成block的時候,傳遞空的image或者imageData
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
//系統(tǒng)為iOS 4+時候,如果應(yīng)用進入后臺,繼續(xù)下載
SDWebImageDownloaderContinueInBackground = 1 << 4,
//存儲在NSHTTPCookieStore的cookies
SDWebImageDownloaderHandleCookies = 1 << 5,
//允許不受信任的SSL證書
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
//將圖片下載放到高優(yōu)先級隊列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
大概了解了整個管理類的功能屬性结窘,我們就從內(nèi)部實現(xiàn)一步步了解起:
(1)管理類的初始化和屬性設(shè)置
這種管理類的初始化也比較簡單很洋,就是單例嘛,因為既然用來管理所有的下載操作隧枫,就必須是唯一的喉磁,總不能創(chuàng)建一個下載操作就有一個管理類吧。
下載操作管理類SDWebImageDownloader會通過sharedDownloader創(chuàng)建一個單例對象官脓,并且在init方法里面初始化一些屬性和網(wǎng)絡(luò)設(shè)置(主要有下載操作隊列协怒、下載會話session、請求頭的設(shè)置以及其他屬性的初始化)卑笨,當(dāng)然也可以通過一些對外的屬性來更改這些設(shè)置孕暇。
+ (SDWebImageDownloader *)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (id)init {
if ((self = [super init])) {
//這個屬性的存在主要用來讓我們自定義一個下載操作類來替換
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
//下載操作隊列,用來管理下載操作
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
//保存所有操作隊列的progressBlock和completedBlock
_URLCallbacks = [NSMutableDictionary new];
//設(shè)置請求頭
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];//請求頭
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
//柵欄隊列湾趾,用來起到加鎖的作用
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
//設(shè)置配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = _downloadTimeout;
//delegateQueue設(shè)置為nil芭商,session就會創(chuàng)建一個串行隊列來執(zhí)行所有的代理方法
self.session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
}
return self;
}
(2)管理類如何管理
之前我們提到的管理類的作用就是管理所有的下載操作,讓多個下載操作能各司其職搀缠,互不干擾铛楣。那它是怎么實現(xiàn)的呢?
通過隊列downloadQueue來管理下載操作
通過將下載操作添加到我們的下載隊列中艺普,我們可以不僅可以對隊列進行整體的操作(暫停簸州,取消),也可以通過taskIdentifier找到對應(yīng)的下載操作進行單獨操作歧譬。通過字典URLCallbacks來保存每一個操作的progressBlock和completedBlock
當(dāng)有多個操作時岸浑,就會有多個progressBlock和completedBlock,所以就需要用一個集合類來保存這些代碼塊瑰步,以便在某個操作能夠找到并執(zhí)行自己的progressBlock和completedBlock矢洲。
這邊字典URLCallbacks的結(jié)構(gòu)是以url字符串為key缩焦,value是progressBlock和completedBlock組成的字典读虏。例子如下
{
"https://ss0uperman/img/c.png":{
"progress":progressBlock,
"completed":completedBlock
}
}
具體的代碼如下:
//創(chuàng)建下載操作
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
//設(shè)置超時時間
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 為了防止重復(fù)的緩存 (NSURLCache + SDImageCache) 。如果有NSURLCache袁滥,就要禁止自己的SDImageCache
// 設(shè)置是SDWebImageDownloaderUseNSURLCache(使用NSURLCache)盖桥,是將request的cachePolicy設(shè)置成按照協(xié)議的緩存策略來定,不然就是忽略本地緩存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
//設(shè)置是否自動發(fā)送cookie
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//形成通道题翻,不比等到響應(yīng)就可發(fā)送下一條請求揩徊,也實現(xiàn)TCP的復(fù)用
request.HTTPShouldUsePipelining = YES;
//設(shè)置請求頭
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
//創(chuàng)建下載操作
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
//下載中部分
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
//完成部分
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
//取消部分
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
//設(shè)置是否解壓圖片
operation.shouldDecompressImages = wself.shouldDecompressImages;
//身份認證
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
//設(shè)置下載操作的優(yōu)先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//添加到下載隊列中
[wself.downloadQueue addOperation:operation];
//設(shè)置隊列的下載順序(其實就是設(shè)置依賴關(guān)系)
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
//將每個下載操作的progressBlock,completedBlock保存起來
//因為是同時有多個下載操作,所以要保持起來塑荒,到時候任務(wù)完成根據(jù)url去找出來調(diào)用
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
//如果url為空熄赡,不用保存操作的回調(diào),也不用下載袜炕,馬上completedBlock返回沒有圖片數(shù)據(jù)
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
//柵欄函數(shù)(里面的代碼類似于一個加鎖的作用)本谜,防止多個線程同時對URLCallbacks進行操作
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// 將每個下載操作的progressBlock,completedBlock保存起來
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;
//第一次下載該url偎窘,才有后續(xù)操作(比如保存回調(diào)乌助,下載),不是第一次就不用了
if (first) {
createCallback();
}
});
}
其中比較新奇的是-
addProgressCallback:completedBlock:forURL:createCallback:的寫法陌知,看起來挺奇怪的他托,其實就是將將每個下載操作的progressBlock,completedBlock保存在URLCallbacks中仆葡,并且做一個校驗赏参,看是不是第一次下載,是第一次下載才需要通過createCallback回調(diào)繼續(xù)執(zhí)行沿盅。
另外把篓,通過創(chuàng)建一個下載操作,并且把會話session和請求request傳給operation主要是為了讓所有的下載操作共用同一個會話腰涧。在該管理類中韧掩,遵守了NSURLSessionTaskDelegate, 和NSURLSessionDataDelegate協(xié)議,當(dāng)作為代理的自己執(zhí)行代理方法時窖铡,會將代理方法分配給各自的操作類疗锐,讓它們自己去處理下載的協(xié)議。管理類只需要獲取操作類處理好的數(shù)據(jù)就可以了费彼。
② 下載操作類的設(shè)計
(1)NSOperation的認識
講解完下載操作管理類的設(shè)計滑臊,肯定對下載操作類的內(nèi)部的實現(xiàn)有所好奇(其實主要是它怎么處理獲取的數(shù)據(jù))。我們知道下載操作類SDWebImageDownloaderOperation繼承于NSOperation箍铲,在講解之前我們先來了解一下NSOperation雇卷。
NSOperation默認是非并發(fā)的, 也就說如果你把operation放到某個線程執(zhí)行, 它會一直block住該線程, 直到operation finished. 對于非并發(fā)的operation你只需要繼承NSOperation, 然后重寫main()方法就可以了。但是我們需要的是并發(fā)NSOperation颠猴。所以我們需要:
- 重寫isConcurrent函數(shù), 返回YES
- 重寫start()函數(shù)
- 重寫isExecuting和isFinished函數(shù)
NSOperation有三個狀態(tài)量isCancelled, isExecuting和isFinished聋庵。非并發(fā)的話,main函數(shù)執(zhí)行完成后, isExecuting會被置為NO, 而isFinished則被置為YES芙粱。而并發(fā)的話,因為task是異步執(zhí)行的氧映,系統(tǒng)不知道operation什么時候finished春畔,所以需要你手動去管理。
當(dāng)這個操作類和下載關(guān)聯(lián)起來時,我們就在start()函數(shù)中開啟網(wǎng)絡(luò)下載律姨,并設(shè)置isExecuting振峻,在網(wǎng)絡(luò)完成回調(diào)中設(shè)置isFinished。這樣我們就掌握了這個操作類的生命周期择份。
(2)NSOperation的子類SDWebImageDownloaderOperation
接下來回到我們的下載操作類SDWebImageDownloaderOperation扣孟,SDWebImageDownloaderOperation覆寫了父類的這executing和finished兩個只讀屬性,讓他們變成可讀寫的荣赶。
@property (assign, nonatomic, getter = isExecuting) BOOL executing;//是否正在執(zhí)行
@property (assign, nonatomic, getter = isFinished) BOOL finished;//是否完成
SDWebImageDownloaderOperation實際上包含著一個task(NSURLSessionTask類型)凤价,當(dāng)操作開始時(start()執(zhí)行),開始下載拔创,當(dāng)執(zhí)行下載完成的代理方法時或者請求錯誤時利诺,設(shè)置isFinished,也意味著操作的完成剩燥。
(3)SDWebImageDownloaderOperation的狀態(tài)(開始慢逾、取消,結(jié)束)
當(dāng)操作開始執(zhí)行時灭红,start()函數(shù)開始執(zhí)行侣滩,start函數(shù)中主要做了以下幾件事:
- 判斷isCancelled的值,若取消了变擒,就重置屬性君珠,不用繼續(xù)下載
- 申請一段后臺時間來進行下載(如果后臺時間快到,也會將下載停掉)
- 創(chuàng)建會話session赁项,創(chuàng)建一個task請求進行下載葛躏,需要注意的是這邊的會話可能是自身創(chuàng)建的或者是初始化時從管理類傳進來的。
- 設(shè)置executing的值
以上是在給self加鎖的情況下進行的
- 判斷task是否存在悠菜,然后進行對應(yīng)的通知和回調(diào)
- 再次確保后臺任務(wù)標(biāo)識符銷毀(這一步不太清楚其含義敖⒃堋?)
//操作開始執(zhí)行
- (void)start {
@synchronized (self) {
//判斷是否已經(jīng)取消
if (self.isCancelled) {
self.finished = YES;
//重置清空
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
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)];
//申請更長的后臺時間來完成下載悔醋,時間快到時會執(zhí)行block中的代碼
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
// 當(dāng)應(yīng)用程序留給后臺的時間快要結(jié)束時(該時間有限)摩窃,這個block將執(zhí)行:進行一些清理工作(主線程執(zhí)行),清理失敗會導(dǎo)致程序掛掉
if (sself) {
//主要是用來取消下載操作
[sself cancel];
//endBackgroundTask:和beginBackgroundTaskWithExpirationHandler成對出來芬骄,意思是結(jié)束后臺任務(wù)
// 標(biāo)記指定的后臺任務(wù)完成
[app endBackgroundTask:sself.backgroundTaskId];
// 銷毀后臺任務(wù)標(biāo)識符
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
//創(chuàng)建會話session
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
//創(chuàng)建task請求
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];
}
//開始下載任務(wù)
[self.dataTask resume];
//如果task請求存在
if (self.dataTask) {
//下載剛開始猾愿,接收大小(0)和總預(yù)計接收大小未知(-1)
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//主線程回調(diào)下載開始的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
}
else {//task請求不存在
//直接在completedBlock回調(diào)錯誤信息
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
// 確保后臺任務(wù)標(biāo)識符銷毀
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
取消的話都會執(zhí)行到cancelInternal函數(shù)账阻,取消內(nèi)部task蒂秘,并且進行對應(yīng)回調(diào),重設(shè)狀態(tài)值淘太。
//取消的內(nèi)部操作
- (void)cancelInternal {
//如果已完成就不用取消了
if (self.isFinished) return;
[super cancel];
//如果有取消后的代碼塊姻僧,就執(zhí)行
if (self.cancelBlock) self.cancelBlock();
if (self.dataTask) {
[self.dataTask cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// As we cancelled the connection, 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];
}
至于結(jié)束有兩種情況规丽,當(dāng)執(zhí)行下載完成的代理方法時或者請求錯誤時,主要都在會話session的代理方法中執(zhí)行撇贺,可以通過下面下載過程的講解來認識赌莺。
(3)SDWebImageDownloaderOperation下載過程
為什么講下載過程呢?因為其實SDWebImageDownloaderOperation把下載封裝起來松嘶,一個操作對應(yīng)一個下載艘狭,下載過程對數(shù)據(jù)的處理都是在SDWebImageDownloaderOperation中。
SDWebImageDownloaderOperation遵守了NSURLSessionTaskDelegate, 和NSURLSessionDataDelegate協(xié)議翠订。
當(dāng)session的值不為nil巢音,意味著用管理類傳進來共用的會話session,當(dāng)session的值為nil蕴轨,就需要在操作類中創(chuàng)建一個私有的會話session港谊。不管是用誰的session,都會實現(xiàn)NSURLSession的幾個代理方法橙弱。
NSURLSessionDataDelegate主要實現(xiàn)了三個方法:
- dataTask收到響應(yīng)的代理方法歧寺。(告訴delegate已經(jīng)接收到服務(wù)器的初始應(yīng)答, 接下來準(zhǔn)備數(shù)據(jù)任務(wù)的操作)
在接收到服務(wù)器的響應(yīng)時,通過判斷響應(yīng)的狀態(tài)碼和期望收到的內(nèi)容大小來判斷是否有數(shù)據(jù)棘脐。
有數(shù)據(jù)的情況就直接保存一些相關(guān)的參數(shù)斜筐,比如期望的數(shù)據(jù)大小,響應(yīng)蛀缝。沒數(shù)據(jù)的情況又分成請求失敗和304(沒有更新)顷链,這兩種都是會取消下載。
// 告訴delegate已經(jīng)接收到服務(wù)器的初始應(yīng)答, 接下來準(zhǔn)備數(shù)據(jù)任務(wù)的操作
// 在這里判斷響應(yīng)屈梁,來看是否有數(shù)據(jù)下載
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
//'304 Not Modified' is an exceptional one
// HTTP狀態(tài)碼:200+正常成功的嗤练;300+重定向;400+請求錯誤;500+一般時服務(wù)端的問題在讶;304 — 未修改煞抬,文檔沒有改變。
// 意思是:小于400除了304都是請求成功构哺,有數(shù)據(jù)
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
if (self.progressBlock) {
self.progressBlock(0, expected);
}
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response = response;
//在主線程回調(diào)收到響應(yīng)的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
else {//沒有數(shù)據(jù)的情況下(一個是304革答,一個是請求錯誤)
NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
//304意味著數(shù)據(jù)沒有改變,我們要先取消再從緩存中獲取
if (code == 304) {
[self cancelInternal];
} else {
[self.dataTask cancel];
}
//在主線程回調(diào)停止下載的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
}
[self done];
}
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
- dataTask接收到數(shù)據(jù)的代理方法
收到數(shù)據(jù)一個默認的操作是將收到的數(shù)據(jù)保存起來曙强,通過progressBlock傳出所受到的數(shù)據(jù)長度和期望收到的總長度残拐。
如果是你是漸進式下載的話(SDWebImageDownloaderProgressiveDownload),就需要把目前收到的data轉(zhuǎn)成image碟嘴,然后通過completedBlock將image回傳出去溪食。所以說completedBlock不是只單單傳最后完成的回調(diào)。
//接收到數(shù)據(jù)的回調(diào)(可能回調(diào)多次)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//拼接到imageData中
[self.imageData appendData:data];
//如果是漸進式的設(shè)置 && 可以收到數(shù)據(jù)
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// 下載數(shù)據(jù)的總大小
const NSInteger totalSize = self.imageData.length;
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
// width + height == 0意味著還沒數(shù)據(jù)(第一次接收到數(shù)據(jù))
if (width + height == 0) {
//獲取圖片資源的屬性字典
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
//獲取高度
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
//賦值給height
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
//獲取寬度
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
//賦值給width
if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
//獲取方向
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
//賦值給orientationValue
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
//釋放屬性字典
CFRelease(properties);
// When we draw to Core Graphics, we lose orientation information,
// which means the image below born of initWithCGIImage will be
// oriented incorrectly sometimes. (Unlike the image born of initWithData
// in didCompleteWithError.) So save it here and pass it on later.
//當(dāng)我們畫到Core Graphics(initWithCGIImage)娜扇,我們會失去我們的方向信息眠菇,所以我們要保存起來
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
// 接收到數(shù)據(jù)并且還沒接收完所有的
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image(局部數(shù)據(jù))
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 解決iOS平臺圖片失真問題
// 因為如果下載的圖片是非png格式边败,圖片會出現(xiàn)失真
// 為了解決這個問題,先將圖片在bitmap的context下渲染
if (partialImageRef) {
//局部數(shù)據(jù)的高度
const size_t partialHeight = CGImageGetHeight(partialImageRef);
// 創(chuàng)建rgb空間
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 獲取上下文 bmContext
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
//繪制圖片到context中
//這里的高度為partialHeight 因為height只在寬高都等于0的時候才進行的賦值捎废,所以以后的情況下partialHeight都等于0,所以要使用當(dāng)前數(shù)據(jù)(imageData)轉(zhuǎn)化的圖片的高度致燥,partialImageRef為要繪制的image
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
//獲取繪制的圖片
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) {
//轉(zhuǎn)化成UIImage
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
//獲取圖片的key登疗,其實就是url
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 對圖片進行處理(使用@2x或@3x)
UIImage *scaledImage = [self scaledImageForKey:key image:image];
//解壓圖片
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
//主線程從completedBlock將image回傳出去(這邊只傳出image,是還未完成的)
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
//傳出progressBlock(只要執(zhí)行這個方法都會傳出)
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
- dataTask將要緩存響應(yīng)的代理方法
// 告訴delegate是否把response存儲到緩存中
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
//如果這個方法執(zhí)行嫌蚤,意味著響應(yīng)不會從緩存獲取
responseFromCached = NO;
NSCachedURLResponse *cachedResponse = proposedResponse;
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// 防止緩存響應(yīng)
cachedResponse = nil;
}
if (completionHandler) {
completionHandler(cachedResponse);
}
}
NSURLSessionTaskDelegate主要實現(xiàn)了兩個方法:
- task完成執(zhí)行的代理方法
下載完成會發(fā)出通知(有錯誤的情況值是Stop辐益,沒錯誤才會發(fā)出Stop和Finished)
另外有錯誤的情況下,只需要回調(diào)錯誤信息脱吱。沒錯誤的情況的情況下還得判斷是否有imageData智政,有的話將imageData轉(zhuǎn)成image,然后進行對應(yīng)操作(變成對應(yīng)大小箱蝠,解壓)续捂,再通過completionBlock傳出去。
執(zhí)行完上面的步驟后宦搬,將completionBlock置為nil牙瓢,以免造成開發(fā)者的循環(huán)引用。并且將相關(guān)的屬性重置清空间校。
//下載完成的回調(diào)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
@synchronized(self) {
self.thread = nil;
self.dataTask = nil;
dispatch_async(dispatch_get_main_queue(), ^{
//發(fā)出停止的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (!error) {
//如果沒有error則再發(fā)出完成的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
}
});
}
//如果有error矾克,就沒返回數(shù)據(jù),只返回error和finish的標(biāo)示
if (error) {
if (self.completedBlock) {
self.completedBlock(nil, nil, error, YES);
}
} else {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
if (completionBlock) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
//設(shè)置是忽略緩存響應(yīng) && 從緩存獲取響應(yīng) && url緩存中找得到self.request的緩存
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {//有imageData
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
//通過url的判斷(包含@2x.@3x.)將圖片轉(zhuǎn)換成對應(yīng)大小
image = [self scaledImageForKey:key image:image];
// 只解壓圖片憔足,不解壓gif
if (!image.images) {
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
//解壓的圖片的size等于0
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {//解壓的圖片的size正常
completionBlock(image, self.imageData, nil, YES);
}
} else {//沒有imageData
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
}
self.completionBlock = nil;
[self done];
}
- task收到挑戰(zhàn)執(zhí)行的代理方法
//告訴delegate, task已收到授權(quán):處理服務(wù)器返回的證書, 需要在該方法中告訴系統(tǒng)是否需要安裝服務(wù)器返回的證書
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// 判斷服務(wù)器返回的證書是否是服務(wù)器信任的
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//如果設(shè)置不是忽略非法SSL證書
if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
//使用默認處置方式
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
} else {
//使用自簽證書
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
} else {
//之前驗證失敗次數(shù)等于0
if ([challenge previousFailureCount] == 0) {
if (self.credential) {
//使用自簽證書
credential = self.credential;
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
//取消證書驗證
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
//取消證書驗證
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
//安裝證書
if (completionHandler) {
completionHandler(disposition, credential);
}
}