【源碼解讀】SDWebImage ─── 下載器的設(shè)計

一. 下載器的介紹

下載器在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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胁附,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子滓彰,更是在濱河造成了極大的恐慌控妻,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件找蜜,死亡現(xiàn)場離奇詭異饼暑,居然都是意外死亡,警方通過查閱死者的電腦和手機洗做,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門弓叛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诚纸,你說我怎么就攤上這事撰筷。” “怎么了畦徘?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵毕籽,是天一觀的道長抬闯。 經(jīng)常有香客問我,道長关筒,這世上最難降的妖魔是什么溶握? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮蒸播,結(jié)果婚禮上睡榆,老公的妹妹穿的比我還像新娘。我一直安慰自己袍榆,他們只是感情好胀屿,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著包雀,像睡著了一般宿崭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上才写,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天葡兑,我揣著相機與錄音,去河邊找鬼琅摩。 笑死铁孵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的房资。 我是一名探鬼主播蜕劝,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼轰异!你這毒婦竟也來了岖沛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤搭独,失蹤者是張志新(化名)和其女友劉穎婴削,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牙肝,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡唉俗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了配椭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虫溜。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖股缸,靈堂內(nèi)的尸體忽然破棺而出衡楞,到底是詐尸還是另有隱情,我是刑警寧澤敦姻,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布瘾境,位于F島的核電站歧杏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏迷守。R本人自食惡果不足惜犬绒,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盒犹。 院中可真熱鬧懂更,春花似錦、人聲如沸急膀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卓嫂。三九已至,卻和暖如春聘殖,著一層夾襖步出監(jiān)牢的瞬間晨雳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工奸腺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留餐禁,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓突照,卻偏偏與公主長得像帮非,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子讹蘑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容