SDWebImage源碼解讀之SDWebImageDownloaderOperation

第七篇

前言

本篇文章主要講解下載操作的相關(guān)知識(shí),SDWebImageDownloaderOperation的主要任務(wù)是把一張圖片從服務(wù)器下載到內(nèi)存中。下載數(shù)據(jù)并不難,如何對(duì)下載這一系列的任務(wù)進(jìn)行設(shè)計(jì),就很難了。接下來我們一步一步的分析作者在開發(fā)中的思路和使用到的技術(shù)細(xì)節(jié)。

NSOperation

NSOperation想必大家都知道,為了讓程序執(zhí)行的更快,我們用多線程異步的方式解決這個(gè)問題,GCDNSOperation都能實(shí)現(xiàn)多線程,我們這里只介紹NSOperation。如果大家想了解更多NSOperation的知識(shí),我覺得這篇文章寫得挺好:多線程之NSOperation簡介

我們把NSOperation最核心的使用方法總結(jié)一下:

  1. NSOperation有兩個(gè)方法:main()start()。如果想使用同步,那么最簡單方法的就是把邏輯寫在main()中,使用異步,需要把邏輯寫到start()中,然后加入到隊(duì)列之中。
  2. 大家有沒有想過NSOperation什么時(shí)候執(zhí)行呢?按照正常想法,難道要我們自己手動(dòng)調(diào)用main()start()嗎?這樣肯定也是行的。當(dāng)手動(dòng)調(diào)用start()或者main()方法的時(shí)候,和調(diào)用普通的方法沒什么區(qū)別,當(dāng)被加入operationQueue中后,情況不同,operationQueue中所有的NSOperation都是異步執(zhí)行的,也就是說start()會(huì)在子線程執(zhí)行,至于是串行還是并發(fā),都由maxConcurrentOperationCount控制,當(dāng)maxConcurrentOperationCount == 1時(shí),相當(dāng)于串行了。另外一種方法就是加入到operationQueue中,operationQueue會(huì)盡快執(zhí)行NSOperation,如果operationQueue是同步的,那么它會(huì)等到NSOperation的isFinished等于YES后,在執(zhí)行下一個(gè)任務(wù),如果是異步的,通過設(shè)置maxConcurrentOperationCount來控制同事執(zhí)行的最大操作,某個(gè)操作完成后,繼續(xù)其他的操作。
  3. 并不是調(diào)用了canche就一定取消了,如果NSOperation沒有執(zhí)行,那么就會(huì)取消,如果執(zhí)行了,只會(huì)將isCancelled設(shè)置為YES。所以,在我們的操作中,我們應(yīng)該在每個(gè)操作開始前,或者在每個(gè)有意義的實(shí)際操作完成后,先檢查下這個(gè)屬性是不是已經(jīng)設(shè)置為YES。如果是YES,則后面操作都可以不用在執(zhí)行了。

能夠引起思考的地方就是,比如說我有一系列的任務(wù)要執(zhí)行,我有兩種選擇,一種是通過數(shù)組控制數(shù)據(jù)的取出順序,另外一種就是使用隊(duì)列

通知

extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
extern NSString * _Nonnull const SDWebImageDownloadFinishNotification;

SDWebImageDownloaderOperation有四種情況會(huì)發(fā)送通知:

  1. 任務(wù)開始
  2. 接收到數(shù)據(jù)
  3. 暫停
  4. 完成

不知道大家發(fā)現(xiàn)沒有,在設(shè)計(jì)一個(gè)功能的時(shí)候,作者都會(huì)用通知的形式暴露出關(guān)鍵的節(jié)點(diǎn)。不管使用者需不需要使用這些通知。這是一個(gè)很好地方法,可以在自定義控件的時(shí)候參考這個(gè)設(shè)計(jì)。

SDWebImageDownloaderOperationInterface

/**
 Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol
 */
@protocol SDWebImageDownloaderOperationInterface<NSObject>

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;

- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;

@end

按照作者的注釋,如果我們想要實(shí)現(xiàn)一個(gè)自定義的下載操作,就必須繼承自NSOperation,同時(shí)實(shí)現(xiàn)SDWebImageDownloaderOperationInterface這個(gè)協(xié)議,我們不去看其他的代碼,只做一個(gè)簡單的猜測:很可能在別的類中,只使用SDWebImageDownloaderOperationInterfaceNSOperation中的方法和屬性。

  • 使用NSURLRequest,NSURLSessionSDWebImageDownloaderOptions初始化
  • 可以為每一個(gè)NSOperation自由的添加相應(yīng)對(duì)象
  • 設(shè)置是否需要解壓圖片
  • 設(shè)置是否需要設(shè)置憑證

@interface SDWebImageDownloaderOperation : NSOperation

關(guān)于SDWebImageDownloaderOperation.h的設(shè)計(jì),有幾點(diǎn)值得我們注意,首先它是遵守SDWebImageDownloaderOperationInterface協(xié)議的,所以上一節(jié)的那些方法,都必須實(shí)現(xiàn),我們?cè)谠O(shè)計(jì)這個(gè).h的時(shí)候呢,可以把協(xié)議中的方法再次寫到這個(gè).h中,這樣別人在使用的時(shí)候,就會(huì)更加直觀

/**
 * The credential used for authentication challenges in `-connection:didReceiveAuthenticationChallenge:`.
 *
 * This will be overridden by any shared credentials that exist for the username or password of the request URL, if present.
 */
@property (nonatomic, strong, nullable) NSURLCredential *credential;

通過聲明一個(gè)屬性,就實(shí)現(xiàn)了SDWebImageDownloaderOperationInterface協(xié)議中的

- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;

一般情況下,我們?cè)谥鲃?dòng)指明初始化方法的時(shí)候,肯定會(huì)為初始化方法設(shè)定幾個(gè)參數(shù)。那么這些參數(shù)就應(yīng)該以只讀的方式暴露給他人。比如:

初始化方法:

/**
 *  Initializes a `SDWebImageDownloaderOperation` object
 *
 *  @see SDWebImageDownloaderOperation
 *
 *  @param request        the URL request
 *  @param session        the URL session in which this operation will run
 *  @param options        downloader options
 *
 *  @return the initialized instance
 */
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;

只讀的屬性:

/**
* The request used by the operation's task.
*/
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;

/**
* The operation's task
*/
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
/**
* The SDWebImageDownloaderOptions for the receiver.
*/
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;

其他的屬性:

/**
 * The expected size of data.
 */
@property (assign, nonatomic) NSInteger expectedSize;

/**
 * The response returned by the operation's connection.
 */
@property (strong, nonatomic, nullable) NSURLResponse *response;

取消方法:

/**
 *  Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
 *
 *  @param token the token representing a set of callbacks to cancel
 *
 *  @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise.
 */
- (BOOL)cancel:(nullable id)token;

這個(gè)方法不是取消任務(wù)的,而是取消任務(wù)中的響應(yīng),當(dāng)時(shí)當(dāng)任務(wù)中沒有響應(yīng)者的時(shí)候,任務(wù)也會(huì)被取消。

SDWebImageDownloaderOperation.m

我們的目的是下載一張圖片,那么我們最核心的邏輯是什么呢?

  1. 初始化一個(gè)task
  2. 添加響應(yīng)者
  3. 開啟下載任務(wù)
  4. 處理下載過程和結(jié)束后的事情

也就是說.m中所有的代碼,都是圍繞著上邊4點(diǎn)來設(shè)計(jì)的。 那么我們就詳細(xì)的對(duì)每一步進(jìn)行分析:

1.初始化一個(gè)task

- (nonnull instancetype)init {
    return [self initWithRequest:nil inSession:nil options:0];
}

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        _options = options;
        _callbackBlocks = [NSMutableArray new];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: is called or not called
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)dealloc {
    SDDispatchQueueRelease(_barrierQueue);
}

這個(gè)初始化方法里邊有很多我們?cè)?h沒有見過的屬性。我們有必要在此做一些解釋,在接下來的解讀中,就不會(huì)再做出解釋了。

  • _callbackBlocks @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks; 是一個(gè)數(shù)組,數(shù)組中存放的是SDCallbacksDictionary類型的數(shù)據(jù),那么這個(gè)SDCallbacksDictionary其實(shí)就是一個(gè)字典,typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;,key是一個(gè)字符串,這個(gè)字符串有兩種情況:kProgressCallbackKeykCompletedCallbackKey,也就是說進(jìn)度和完成的回調(diào)都是放到一個(gè)數(shù)組中的。那么字典的值就是回調(diào)的block了。
  • _unownedSession @property (weak, nonatomic, nullable) NSURLSession *unownedSession;,這個(gè)屬性是我們初始化時(shí)候傳進(jìn)來的參數(shù),作者提到。這個(gè)參數(shù)不一定是可用的。也就是說是不安全的。當(dāng)出現(xiàn)不可用的情況的時(shí)候,就需要使用@property (strong, nonatomic, nullable) NSURLSession *ownedSession;.
  • responseFromCached 用于設(shè)置是否需要緩存響應(yīng),默認(rèn)為YES
  • _barrierQueue 隊(duì)列,這個(gè)會(huì)在后邊的使用中講解到

2.添加響應(yīng)者

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

看這段代碼,也很好理解。就是把字典添加到數(shù)組中去,但是這里邊有一個(gè)很重要的知識(shí)點(diǎn):dispatch_barrier_async,我們做一個(gè)簡單的介紹。

我們可以創(chuàng)建兩種類型的隊(duì)列,串行和并行,也就是DISPATCH_QUEUE_SERIAL,DISPATCH_QUEUE_CONCURRENT。那么dispatch_barrier_async和dispatch_barrier_sync究竟有什么不同之處呢?

barrier這個(gè)詞是柵欄的意思,也就是說是用來做攔截功能的,上邊的這另種都能夠攔截任務(wù),換句話說,就是只有我的任務(wù)完成后,隊(duì)列后邊的任務(wù)才能完成。

不同之處就是,dispatch_barrier_sync控制了任務(wù)往隊(duì)列添加這一過程,只有當(dāng)我的任務(wù)完成之后,才能往隊(duì)列中添加任務(wù)。dispatch_barrier_async不會(huì)控制隊(duì)列添加任務(wù)。但是只有當(dāng)我的任務(wù)完成后,隊(duì)列中后邊的任務(wù)才會(huì)執(zhí)行。

那么在這里的任務(wù)是往數(shù)組中添加數(shù)據(jù),對(duì)順序沒什么要求,我們采取dispatch_barrier_async就可以了,已經(jīng)能保證數(shù)據(jù)添加的安全性了。

- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        // We need to remove [NSNull null] because there might not always be a progress block for each callback
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy];    // strip mutability here
}

這個(gè)方法是根據(jù)key取出所有符合key的block,這里采用了同步的方式,相當(dāng)于加鎖。比較有意思的是[self.callbackBlocks valueForKey:key]這段代碼,self.callbackBlocks是一個(gè)數(shù)組,我們假定他的結(jié)構(gòu)是這樣的:

@[@{@"completed" : Block1}, 
@{@"progress" : Block2}, 
@{@"completed" : Block3}, 
@{@"progress" : Block4}, 
@{@"completed" : Block5}, 
@{@"progress" : Block6}]

調(diào)用[self.callbackBlocks valueForKey:@"progress"]后會(huì)得到[Block2, Block4, Block6].
removeObjectIdenticalTo:這個(gè)方法會(huì)移除數(shù)組中指定相同地址的元素。

- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

這個(gè)函數(shù),就是取消某一回調(diào)。使用了dispatch_barrier_sync,保證,必須該隊(duì)列之前的任務(wù)都完成,且該取消任務(wù)結(jié)束后,在將其他的任務(wù)加入隊(duì)列。

3.開啟下載任務(wù)

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        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;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    
    [self.dataTask resume];

    if (self.dataTask) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

#if SD_UIKIT
    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
}

這一塊也分為幾個(gè)子任務(wù)

  • 如果該任務(wù)已經(jīng)被設(shè)置為取消了,那么就無需開啟下載任務(wù)了。并重置。別忘了設(shè)置finished為YES

      - (void)reset {
          dispatch_barrier_async(self.barrierQueue, ^{
              [self.callbackBlocks removeAllObjects];
          });
          self.dataTask = nil;
          self.imageData = nil;
          if (self.ownedSession) {
              [self.ownedSession invalidateAndCancel];
              self.ownedSession = nil;
          }
      }
    
  • 確保能夠開啟下載任務(wù),我之前在網(wǎng)上跟別的哥們討論過,一開始不太明白下邊的方法的用途,后來想通了,也不知道對(duì)不對(duì),start方法的目的只是開啟下載任務(wù),它所要保證的就是調(diào)用start時(shí),任務(wù)能夠開啟,至于是否下載成功,那不是start 應(yīng)該關(guān)心的事情。

      #if SD_UIKIT
              Class UIApplicationClass = NSClassFromString(@"UIApplication");
              BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
              if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
                  __weak __typeof__ (self) wself = self;
                  UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
                  self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                      __strong __typeof (wself) sself = wself;
      
                      if (sself) {
                          [sself cancel];
      
                          [app endBackgroundTask:sself.backgroundTaskId];
                          sself.backgroundTaskId = UIBackgroundTaskInvalid;
                      }
                  }];
              }
      #endif
      
      開啟后,確保關(guān)閉后臺(tái)任務(wù)
      
      #if SD_UIKIT
          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
    
  • task開啟前的準(zhǔn)備工作

       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;
      }
      
      self.dataTask = [session dataTaskWithRequest:self.request];
      self.executing = YES;
    
  • 開啟task 并處理回調(diào)

     [self.dataTask resume];
    
      if (self.dataTask) {
          for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
              progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
          }
          dispatch_async(dispatch_get_main_queue(), ^{
              [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
          });
      } else {
          [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
      }
    

4.處理下載過程和結(jié)束后的事情

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //'304 Not Modified' is an exceptional one
    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;
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
        
        //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
        //In case of 304 we need just cancel the operation and return cached image from the cache.
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.dataTask cancel];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
        
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];

        [self done];
    }
    
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

上邊的代碼,處理了當(dāng)收到響應(yīng)后要做的事情。我們規(guī)定,把沒有收到響應(yīng)碼或者響應(yīng)碼小于400認(rèn)定為正常的情況,其中304比較特殊,因?yàn)楫?dāng)stateCode為304的時(shí)候,便是這個(gè)響應(yīng)沒有變化,可以再緩存中讀取。那么其他的情況,就可以認(rèn)定為錯(cuò)誤的請(qǐng)求。

當(dāng)一切順利的時(shí)候,基本上就是給早已定義的屬性賦值,上邊的代碼邏輯比較簡單,在這里就不做介紹了。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf

        // Get the total bytes downloaded
        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);

        if (width + height == 0) {
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                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.
#if SD_UIKIT || SD_WATCH
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
            }
        }

        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#if SD_UIKIT || SD_WATCH
            // Workaround for iOS anamorphic image
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    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) {
#if SD_UIKIT || SD_WATCH
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
#elif SD_MAC
                UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }

        CFRelease(imageSource);
    }

    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

上邊的方法處理了接收到數(shù)據(jù)的邏輯。按照正常想法,當(dāng)我們接受到數(shù)據(jù)的時(shí)候,只要把收據(jù)拼接起來,根據(jù)設(shè)置選項(xiàng),調(diào)用process回調(diào)就行了。那么為什么這個(gè)方法中用了如此大的篇幅來處理圖片數(shù)據(jù)呢?

答案就是,即使圖片沒有下載完,我們也能根據(jù)已經(jīng)獲取的圖片數(shù)據(jù),來顯示一張數(shù)據(jù)不完整的圖片。 通過這樣一個(gè)細(xì)節(jié),我想到了很多應(yīng)用場景,比如說,之前看到過一個(gè)場景,通過滑動(dòng)slider 自上而下的顯示一張圖片的部分內(nèi)容,我們完全可以通過上邊的代碼來實(shí)現(xiàn)。根據(jù)slider的value來控制整個(gè)NSData的大小,來合成圖片。當(dāng)然這也跟圖片的組成有關(guān)。

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

該方法用于響應(yīng)的緩存設(shè)置,如果把回調(diào)的參數(shù)設(shè)置為nil,那么就不會(huì)緩存響應(yīng),總之,真正緩存的數(shù)據(jù)就是回調(diào)中的參數(shù)。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    @synchronized(self) {
        self.dataTask = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            }
        });
    }
    
    if (error) {
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  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
             */
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                // hack
                [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
            } else if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                image = [self scaledImageForKey:key image:image];
                
                // Do not force decoding animated GIFs
                if (!image.images) {
                    if (self.shouldDecompressImages) {
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
#endif
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                } else {
                    [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    [self done];
}

該方法是處理了圖片下載完成之后的邏輯,也沒有很特別的東西,比較復(fù)雜的是對(duì)完成后的數(shù)據(jù)的處理更加完善。要做到這一點(diǎn),確實(shí)需要N多知識(shí)的積累啊。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        if (challenge.previousFailureCount == 0) {
            if (self.credential) {
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

這個(gè)方法跟HTTPS有點(diǎn)關(guān)系,要想說明白這個(gè)方法究竟干了什么事? 需要對(duì)驗(yàn)證有點(diǎn)了解才行。

當(dāng)我們發(fā)出了一個(gè)請(qǐng)求,這個(gè)請(qǐng)求到達(dá)服務(wù)器后,假定服務(wù)器設(shè)置了需要驗(yàn)證。那么這個(gè)方法就會(huì)被調(diào)用。服務(wù)器會(huì)返回去一個(gè)NSURLAuthenticationChallenge。通過NSURLAuthenticationChallengeprotectionSpace,獲取授權(quán)method。如果這個(gè)metho是服務(wù)器信任的, 那么我們就可以直接使用服務(wù)器返回的證書,當(dāng)然,我們也可以使用自己的證書,其他情況都會(huì)被認(rèn)為驗(yàn)證失敗,當(dāng)前請(qǐng)求將會(huì)被取消。當(dāng)有了證書后,客戶端就可以使用證書中的公鑰對(duì)數(shù)據(jù)進(jìn)行加密了。

其他的方法:

#if SD_UIKIT || SD_WATCH
+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
    switch (value) {
        case 1:
            return UIImageOrientationUp;
        case 3:
            return UIImageOrientationDown;
        case 8:
            return UIImageOrientationLeft;
        case 6:
            return UIImageOrientationRight;
        case 2:
            return UIImageOrientationUpMirrored;
        case 4:
            return UIImageOrientationDownMirrored;
        case 5:
            return UIImageOrientationLeftMirrored;
        case 7:
            return UIImageOrientationRightMirrored;
        default:
            return UIImageOrientationUp;
    }
}
#endif

- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

- (BOOL)shouldContinueWhenAppEntersBackground {
    return self.options & SDWebImageDownloaderContinueInBackground;
}

- (void)callCompletionBlocksWithError:(nullable NSError *)error {
    [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
}

- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

總結(jié)

SDWebImageDownloaderOperation提供了下載單張圖片的能力,在真實(shí)開發(fā)中。圖片往往都是一組一組的出現(xiàn)的。那么該如何管理這些組圖片呢?下一篇文章將會(huì)揭曉答案。

由于個(gè)人知識(shí)有限,如有錯(cuò)誤之處,還望各路大俠給予指出啊

  1. SDWebImage源碼解讀 之 NSData+ImageContentType 簡書 博客園
  2. SDWebImage源碼解讀 之 UIImage+GIF 簡書 博客園
  3. SDWebImage源碼解讀 之 SDWebImageCompat 簡書 博客園
  4. SDWebImage源碼解讀 之SDWebImageDecoder 簡書 博客園
  5. SDWebImage源碼解讀 之SDWebImageCache(上) 簡書 博客園
  6. SDWebImage源碼解讀之SDWebImageCache(下) 簡書 博客園
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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