你真的懂SDWebImage?

SDWebImage已經到了用爛了的地步,對於一名優秀的開發者來講,會用只是最簡單的一步,咱們要可以研究到其底層的技術實現和設計思路原理。在網上偶然間看到了一篇文章,感受不錯,略做修改,批註,後面的內容你們能夠一塊兒探討~css

 

源碼來源:  https://github.com/rs/SDWebImagegit

版本: 3.7github

SDWebImage是一個開源的第三方庫,它提供了UIImageView的一個分類,以支持從遠程服務器下載並緩存圖片的功能。它具備如下功能(github上的自我介紹):數組

  1. 提供UIImageView的一個分類,以支持網絡圖片的加載與緩存管理
  2. 一個異步的圖片加載器
  3. 一個異步的內存+磁盤圖片緩存
  4. 支持GIF圖片
  5. 支持WebP圖片
  6. 後臺圖片解壓縮處理
  7. 確保同一個URL的圖片不被下載屢次(操做隊列)
  8. 確保虛假的URL不會被反覆加載
  9. 確保下載及緩存時,主線程不被阻塞(寫到磁盤時採用異步)

從github上對SDWebImage使用狀況就能夠看出,SDWebImage在圖片下載及緩存的處理方面仍是很被承認的。在本文中,咱們主要從源碼的角度來分析一下SDWebImage的實現機制。討論的內容將主要集中在圖片的下載及緩存,而不包含對GIF圖片及WebP圖片的支持操做。瀏覽器

1、下載

在SDWebImage中,圖片的下載是由SDWebImageDownloader類來完成的。它是一個異步下載器,並對圖像加載作了優化處理。下面咱們就來看看它的具體實現。緩存

一、下載選項

在下載的過程當中,程序會根據設置的不一樣的下載選項,而執行不一樣的操做。下載選項由枚舉SDWebImageDownloaderOptions定義,具體以下安全

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
  SDWebImageDownloaderLowPriority = 1 << 0,
  SDWebImageDownloaderProgressiveDownload = 1 << 1,
  // 默認狀況下請求不使用NSURLCache,若是設置該選項,則以默認的緩存策略來使用NSURLCache
  SDWebImageDownloaderUseNSURLCache = 1 << 2,
  // 若是從NSURLCache緩存中讀取圖片,則使用nil做爲參數來調用完成block
  SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
  // 在iOS 4+系統上,容許程序進入後臺後繼續下載圖片。該操做經過向系統申請額外的時間來完成後臺下載。若是後臺任務終止,則操做會被取消
  SDWebImageDownloaderContinueInBackground = 1 << 4,
  // 經過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲在NSHTTPCookieStore中的cookie
  SDWebImageDownloaderHandleCookies = 1 << 5,
  // 容許不受信任的SSL證書。主要用於測試目的。
  SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
  // 將圖片下載放到高優先級隊列中
  SDWebImageDownloaderHighPriority = 1 << 7,
};

 

能夠看出,這些選項主要涉及到下載的優先級、緩存、後臺任務執行、cookie處理以認證幾個方面。服務器

二、下載順序

SDWebImage的下載操做是按必定順序來處理的,它定義了兩種下載順序,以下所示cookie

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {  // 以隊列的方式,按照先進先出的順序下載。這是默認的下載順序  SDWebImageDownloaderFIFOExecutionOrder,  // 以棧的方式,按照後進先出的順序下載。(以添加操做依賴的方式實現)  SDWebImageDownloaderLIFOExecutionOrder }; 

三、下載管理器

SDWebImageDownloader下載管理器是一個單例類,它主要負責圖片的下載操做的管理。圖片的下載是放在一個NSOperationQueue操做隊列中來完成的,其聲明以下:網絡

@property (strong, nonatomic) NSOperationQueue *downloadQueue; 

默認狀況下,隊列最大併發數是6。若是須要的話,咱們能夠經過SDWebImageDownloader類的  maxConcurrentDownloads 屬性來修改。 

全部下載操做的網絡響應序列化處理是放在一個自定義的並行調度隊列中來處理的,其聲明及定義以下:

@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
- (id)init {
  if ((self = [super init])) {
    ...
    _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    ...
  }
  return self;
}

 

每個圖片的下載都會對應一些回調操做,以下載進度回調,下載完成回調等,這些回調操做是以block形式來呈現,爲此在SDWebImageDownloader.h中定義了幾個block,以下所示:

// 下載進度 typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize); // 下載完成 typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished); // Header過濾 typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers); 

圖片下載的這些回調信息存儲在SDWebImageDownloader類的  URLCallbacks 屬性中,該屬性是一個字典,key是圖片的URL地址,value則是一個數組,包含每一個圖片的多組回調信息。因爲咱們容許多個圖片同時下載,所以可能會有多個線程同時操做URLCallbacks屬性。爲了保證URLCallbacks操做(添加、刪除)的線程安全性,SDWebImageDownloader將這些操做做爲一個個任務放到barrierQueue隊列中,並設置屏障來確保同一時間只有一個線程操做URLCallbacks屬性,咱們以添加操做爲例,以下代碼所示: 

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    ...
    // 1. 以dispatch_barrier_sync操做來保證同一時間只有一個線程能對URLCallbacks進行操做
    dispatch_barrier_sync(self.barrierQueue, ^{
  ...
  // 2. 處理同一URL的同步下載請求的單個下載
  NSMutableArray *callbacksForURL = self.URLCallbacks[url];
  NSMutableDictionary *callbacks = [NSMutableDictionary new];
  if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
  if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
  [callbacksForURL addObject:callbacks];
  self.URLCallbacks[url] = callbacksForURL;
  ...
    });
}

 

整個下載管理器對於下載請求的管理都是放在downloadImageWithURL:options:progress:completed:方法裏面來處理的,該方法調用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法來將請求的信息存入管理器中,同時在建立回調的block中建立新的操做,配置以後將其放入downloadQueue操做隊列中,最後方法返回新建立的操做。其具體實現以下:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
  ...
  [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ...
    // 1. 建立請求對象,並根據options參數設置其屬性
    // 爲了不潛在的重複緩存(NSURLCache + SDImageCache),若是沒有明確告知須要緩存,則禁用圖片請求的緩存操做
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    ...
    // 2. 建立SDWebImageDownloaderOperation操做對象,並進行配置
    // 配置信息包括是否須要認證、優先級
    operation = [[wself.operationClass alloc] initWithRequest:request
                              options:options
                             progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                               // 3. 從管理器的callbacksForURL中找出該URL全部的進度處理回調並調用
                               ...
                               for (NSDictionary *callbacks in callbacksForURL) {
                                 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                 if (callback) callback(receivedSize, expectedSize);
                               }
                             }
                            completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                               // 4. 從管理器的callbacksForURL中找出該URL全部的完成處理回調並調用,
                               // 若是finished爲YES,則將該url對應的回調信息從URLCallbacks中刪除
                              ...
                              if (finished) {
                                [sself removeCallbacksForURL:url];
                              }
                              for (NSDictionary *callbacks in callbacksForURL) {
                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                if (callback) callback(image, data, error, finished);
                              }
                            }
                            cancelled:^{
                              // 5. 取消操做將該url對應的回調信息從URLCallbacks中刪除
                              SDWebImageDownloader *sself = wself;
                              if (!sself) return;
                              [sself removeCallbacksForURL:url];
                            }];
    ...
    // 6. 將操做加入到操做隊列downloadQueue中
    // 若是是LIFO順序,則將新的操做做爲原隊列中最後一個操做的依賴,而後將新操做設置爲最後一個操做
    [wself.downloadQueue addOperation:operation];
    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
      [wself.lastAddedOperation addDependency:operation];
      wself.lastAddedOperation = operation;
    }
  }];
  return operation;
}

 

另外,每一個下載操做的超時時間能夠經過downloadTimeout屬性來設置,默認值爲15秒。

四、下載操做

每一個圖片的下載都是一個Operation操做。咱們在上面分析過這個操做的建立及加入操做隊列的過程。如今咱們來看看單個操做的具體實現。

SDWebImage定義了一個協議,即  SDWebImageOperation 做爲圖片下載操做的基礎協議。它只聲明瞭一個cancel方法,用於取消操做。協議的具體聲明以下: 

@protocol SDWebImageOperation <NSObject> - (void)cancel; @end 

SDWebImage自定義了一個Operation類,即  SDWebImageDownloaderOperation,它繼承自NSOperation,並採用了SDWebImageOperation協議。除了繼承而來的方法,該類只向外暴露了一個方法,即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled:。 

對於圖片的下載,SDWebImageDownloaderOperation徹底依賴於URL加載系統中的NSURLConnection類(並未使用7.0之後的NSURLSession類)。咱們先來分析一下SDWebImageDownloaderOperation類中對於圖片實際數據的下載處理,即NSURLConnection各代理方法的實現。

首先,SDWebImageDownloaderOperation在分類中採用了NSURLConnectionDataDelegate協議,並實現了該協議的如下幾個方法:

- connection:didReceiveResponse: - connection:didReceiveData: - connectionDidFinishLoading: - connection:didFailWithError: - connection:willCacheResponse: - connectionShouldUseCredentialStorage: - connection:willSendRequestForAuthenticationChallenge: 

咱們在此不逐一分析每一個方法的實現,就重點分析一下-connection:didReceiveData:方法。該方法的主要任務是接收數據。每次接收到數據時,都會用現有的數據建立一個CGImageSourceRef對象以作處理。在首次獲取到數據時(width+height==0)會從這些包含圖像信息的數據中取出圖像的長、寬、方向等信息以備使用。然後在圖片下載完成以前,會使用CGImageSourceRef對象建立一個圖片對象,通過縮放、解壓縮操做後生成一個UIImage對象供完成回調使用。固然,在這個方法中還須要處理的就是進度信息。若是咱們有設置進度回調的話,就調用這個進度回調以處理當前圖片的下載進度。

注:縮放操做能夠查看SDWebImageCompat文件中的SDScaledImageForKey函數;解壓縮操做能夠查看SDWebImageDecoder文件+decodedImageWithImage方法

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  // 1. 附加數據
  [self.imageData appendData:data];
  if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
    // 2. 獲取已下載數據總大小
    const NSInteger totalSize = self.imageData.length;
    // 3. 更新數據源,咱們須要傳入全部數據,而不只僅是新數據
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
    // 4. 首次獲取到數據時,從這些數據中獲取圖片的長、寬、方向屬性值
    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);
        ...
        CFRelease(properties);
        // 5. 當繪製到Core Graphics時,咱們會丟失方向信息,這意味着有時候由initWithCGIImage建立的圖片
        //	的方向會不對,因此在這邊咱們先保存這個信息並在後面使用。
        orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
      }
    }
    // 6. 圖片還未下載完成
    if (width + height > 0 && totalSize < self.expectedSize) {
      // 7. 使用現有的數據建立圖片對象,若是數據中存有多張圖片,則取第一張
      CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
      // 8. 適用於iOS變形圖像的解決方案。個人理解是因爲iOS只支持RGB顏色空間,因此在此對下載下來的圖片作個顏色空間轉換處理。
      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
      // 9. 對圖片進行縮放、解碼操做
      if (partialImageRef) {
        UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
        NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
        UIImage *scaledImage = [self scaledImageForKey:key image:image];
        image = [UIImage decodedImageWithImage:scaledImage];
        CGImageRelease(partialImageRef);
        dispatch_main_sync_safe(^{
          if (self.completedBlock) {
            self.completedBlock(image, nil, nil, NO);
          }
        });
      }
    }
    CFRelease(imageSource);
  }
  if (self.progressBlock) {
    self.progressBlock(self.imageData.length, self.expectedSize);
  }
}

 

咱們前面說過SDWebImageDownloaderOperation類是繼承自NSOperation類。它沒有簡單的實現main方法,而是採用更加靈活的start方法,以便本身管理下載的狀態。

在start方法中,建立了咱們下載所使用的NSURLConnection對象,開啓了圖片的下載,同時拋出一個下載開始的通知。固然,若是咱們指望下載在後臺處理,則只須要配置咱們的下載選項,使其包含SDWebImageDownloaderContinueInBackground選項。start方法的具體實現以下:

- (void)start {
  @synchronized (self) {
    // 管理下載狀態,若是已取消,則重置當前下載並設置完成狀態爲YES
    if (self.isCancelled) {
      self.finished = YES;
      [self reset];
      return;
    }
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    // 1. 若是設置了在後臺執行,則進行後臺執行
    if ([self shouldContinueWhenAppEntersBackground]) {
      __weak __typeof__ (self) wself = self;
      self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        ...
        }
      }];
    }
#endif
    self.executing = YES;
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    self.thread = [NSThread currentThread];
  }
  [self.connection start];
  if (self.connection) {
    if (self.progressBlock) {
      self.progressBlock(0, NSURLResponseUnknownLength);
    }
    // 2. 在主線程拋出下載開始通知
    dispatch_async(dispatch_get_main_queue(), ^{
      [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
    });
    // 3. 啓動run loop
    if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
      CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
    }
    else {
      CFRunLoopRun();
    }
    // 4. 若是未完成,則取消鏈接
    if (!self.isFinished) {
      [self.connection cancel];
      [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
    }
  }
  else {
    ... 
  }
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
  if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
    [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
    self.backgroundTaskId = UIBackgroundTaskInvalid;
  }
#endif
}

 

固然,在下載完成或下載失敗後,須要中止當前線程的run loop,清除鏈接,並拋出下載中止的通知。若是下載成功,則會處理完整的圖片數據,對其進行適當的縮放與解壓縮操做,以提供給完成回調使用。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實現。

五、小結

下載的核心其實就是利用NSURLConnection對象來加載數據。每一個圖片的下載都由一個Operation操做來完成,並將這些操做放到一個操做隊列中。這樣能夠實現圖片的併發下載。

 

2、緩存

爲了減小網絡流量的消耗,咱們都但願下載下來的圖片緩存到本地,下次再去獲取同一張圖片時,能夠直接從本地獲取,而再也不從遠程服務器獲取。這樣作的另外一個好處是提高了用戶體驗,用戶第二次查看同一幅圖片時,能快速從本地獲取圖片直接呈現給用戶。

SDWebImage提供了對圖片緩存的支持,而該功能是由SDImageCache類來完成的。該類負責處理內存緩存及一個可選的磁盤緩存。其中磁盤緩存的寫操做是異步的,這樣就不會對UI操做形成影響。

一、內存緩存及磁盤緩存

內存緩存的處理是使用NSCache對象來實現的。NSCache是一個相似於集合的容器。它存儲key-value對,這一點相似於NSDictionary類。咱們一般用使用緩存來臨時存儲短期使用但建立昂貴的對象。重用這些對象能夠優化性能,由於它們的值不須要從新計算。另一方面,這些對象對於程序來講不是緊要的,在內存緊張時會被丟棄。

磁盤緩存的處理則是使用NSFileManager對象來實現的圖片存儲的位置是位於Cache文件夾。另外,SDImageCache還定義了一個串行隊列,來異步存儲圖片

內存緩存與磁盤緩存相關變量的聲明及定義以下:

@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache;
@property (strong, nonatomic) NSString *diskCachePath;
@property (strong, nonatomic) NSMutableArray *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
@end
- (id)initWithNamespace:(NSString *)ns {
    if ((self = [super init])) {
  NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
  ...
  _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
  ...
  // Init the memory cache
  _memCache = [[NSCache alloc] init];
  _memCache.name = fullNamespace;
  // Init the disk cache
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace];
  dispatch_sync(_ioQueue, ^{
      _fileManager = [NSFileManager new];
  });
  ...
    }
    return self;
}

 

SDImageCache提供了大量方法來緩存、獲取、移除及清空圖片。而對於每一個圖片,爲了方便地在內存或磁盤中對它進行這些操做,咱們須要一個key值來索引它。在內存中,咱們將其做爲NSCache的key值,而在磁盤中,咱們用這個key做爲圖片的文件名。對於一個遠程服務器下載的圖片,其url是做爲這個key的最佳選擇了。咱們在後面會看到這個key值的重要性。

二、存儲圖片

咱們先來看看圖片的緩存操做,該操做會在內存中放置一份緩存,而若是肯定須要緩存到磁盤,則將磁盤緩存操做做爲一個task放到串行隊列中處理。在iOS中,會先檢測圖片是PNG仍是JPEG,並將其轉換爲相應的圖片數據,最後將數據寫入到磁盤中(文件名是對key值作MD5摘要後的串)。緩存操做的基礎方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk,它的具體實現以下:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
  ...
  // 1. 內存緩存,將其存入NSCache中,同時傳入圖片的消耗值
  [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
  if (toDisk) {
    // 2. 若是肯定須要磁盤緩存,則將緩存操做做爲一個任務放入ioQueue中
    dispatch_async(self.ioQueue, ^{
      NSData *data = imageData;
      if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
        // 3. 須要肯定圖片是PNG仍是JPEG。PNG圖片容易檢測,由於有一個惟一簽名。PNG圖像的前8個字節老是包含如下值:137 80 78 71 13 10 26 10
        // 在imageData爲nil的狀況下假定圖像爲PNG。咱們將其看成PNG以免丟失透明度。而當有圖片數據時,咱們檢測其前綴,肯定圖片的類型
        BOOL imageIsPng = YES;
        if ([imageData length] >= [kPNGSignatureData length]) {
          imageIsPng = ImageDataHasPNGPreffix(imageData);
        }
        if (imageIsPng) {
          data = UIImagePNGRepresentation(image);
        }
        else {
          data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
        }
#else
        data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
      }
      // 4. 建立緩存文件並存儲圖片
      if (data) {
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
          [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
      }
    });
  }
}

 

三、查詢圖片

若是咱們想在內存或磁盤中查詢是否有key指定的圖片,則能夠分別使用如下方法:

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; - (UIImage *)imageFromDiskCacheForKey:(NSString *)key; 

而若是隻是想查看本地是否存在key指定的圖片,則無論是在內存仍是在磁盤上,則可使用如下方法:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
  ...
  // 1. 首先查看內存緩存,若是查找到,則直接回調doneBlock並返回
  UIImage *image = [self imageFromDiskCacheForKey:key];
  if (image) {
    doneBlock(image, SDImageCacheTypeMemory);
    return nil;
  }
  // 2. 若是內存中沒有,則在磁盤中查找。若是找到,則將其放到內存緩存,並調用doneBlock回調
  NSOperation *operation = [NSOperation new];
  dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
      return;
    }
    @autoreleasepool {
      UIImage *diskImage = [self diskImageForKey:key];
      if (diskImage) {
        CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
        [self.memCache setObject:diskImage forKey:key cost:cost];
      }
      dispatch_async(dispatch_get_main_queue(), ^{
        doneBlock(diskImage, SDImageCacheTypeDisk);
      });
    }
  });
  return operation;
}

 

四、移除圖片

圖片的移除操做則可使用如下方法:

- (void)removeImageForKey:(NSString *)key; - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion; 

咱們能夠選擇同時移除內存及磁盤上的圖片。

五、清理圖片(磁盤)

磁盤緩存圖片的清理操做能夠分爲徹底清空和部分清理。徹底清空操做是直接把緩存的文件夾移除,清空操做有如下兩個方法:

- (void)clearDisk; - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion; 

部分清理則是根據咱們設定的一些參數值來移除一些文件,這裏主要有兩個指標:文件的緩存有效期及最大緩存空間大小。文件的緩存有效期能夠經過maxCacheAge屬性來設置,默認是1周的時間。若是文件的緩存時間超過這個時間值,則將其移除。而最大緩存空間大小是經過maxCacheSize屬性來設置的,若是全部緩存文件的總大小超過這一大小,則會按照文件最後修改時間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小於咱們設置的最大使用空間。清理的操做在-cleanDiskWithCompletionBlock:方法中,其實現以下:

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
  dispatch_async(self.ioQueue, ^{
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    // 1. 該枚舉器預先獲取緩存文件的有用的屬性
    NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                           includingPropertiesForKeys:resourceKeys
                                    options:NSDirectoryEnumerationSkipsHiddenFiles
                                 errorHandler:NULL];
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
    NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
    NSUInteger currentCacheSize = 0;
    // 2. 枚舉緩存文件夾中全部文件,該迭代有兩個目的:移除比過時日期更老的文件;存儲文件屬性以備後面執行基於緩存大小的清理操做
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
      NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
      // 3. 跳過文件夾
      if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
        continue;
      }
      // 4. 移除早於有效期的老文件
      NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
      if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue;
      }
      // 5. 存儲文件的引用並計算全部文件的總大小,以備後用
      NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
      currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
      [cacheFiles setObject:resourceValues forKey:fileURL];
    }
    for (NSURL *fileURL in urlsToDelete) {
      [_fileManager removeItemAtURL:fileURL error:nil];
    }
    // 6.若是磁盤緩存的大小大於咱們配置的最大大小,則執行基於文件大小的清理,咱們首先刪除最老的文件
    if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
      // 7. 以設置的最大緩存大小的一半做爲清理目標
      const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
      // 8. 按照最後修改時間來排序剩下的緩存文件
      NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                              usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                              }];
      // 9. 刪除文件,直到緩存總大小降到咱們指望的大小
      for (NSURL *fileURL in sortedFiles) {
        if ([_fileManager removeItemAtURL:fileURL error:nil]) {
          NSDictionary *resourceValues = cacheFiles[fileURL];
          NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
          currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
          if (currentCacheSize < desiredCacheSize) {
            break;
          }
        }
      }
    }
                if (completionBlock) {
      dispatch_async(dispatch_get_main_queue(), ^{
        completionBlock();
      });
    }
  });
}

 

六、小結

以上分析了圖片緩存操做,固然,除了上面講的幾個操做,SDImageCache類還提供了一些輔助方法。如獲取緩存大小、緩存中圖片的數量、判斷緩存中是否存在某個key指定的圖片。另外,SDImageCache類提供了一個單例方法的實現,因此咱們能夠將其看成單例對象來處理。

彙總一些經常使用接口、屬性:

(1)-getSize  :得到硬盤緩存的大小

(2)-getDiskCount : 得到硬盤緩存的圖片數量

(3)-clearMemory  : 清理全部內存圖片

(4)- removeImageForKey:(NSString *)key  系列的方法 : 從內存、硬盤按要求指定清除圖片

(5)maxMemoryCost  :  保存在存儲器中像素的總和

(6)maxCacheSize  :  最大緩存大小 以字節爲單位。默認沒有設置,也就是爲0,而清理磁盤緩存的先決條件爲self.maxCacheSize > 0,因此0表示無限制。

(7)maxCacheAge : 在內存緩存保留的最長時間以秒爲單位計算,默認是一週

 

 

3、SDWebImageManager

在實際的運用中,咱們並不直接使用SDWebImageDownloader類及SDImageCache類來執行圖片的下載及緩存。爲了方便用戶的使用,SDWebImage提供了SDWebImageManager對象來管理圖片的下載與緩存。並且咱們常常用到的諸如UIImageView+WebCache等控件的分類都是基於SDWebImageManager對象的。該對象將一個下載器和一個圖片緩存綁定在一塊兒,並對外提供兩個只讀屬性來獲取它們,以下代碼所示:

@interface SDWebImageManager : NSObject @property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate; @property (strong, nonatomic, readonly) SDImageCache *imageCache; @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; ... @end 

從上面的代碼中咱們還能夠看到有一個delegate屬性,其是一個id<SDWebImageManagerDelegate>對象。SDWebImageManagerDelegate聲明瞭兩個可選實現的方法,以下所示:

// 控制當圖片在緩存中沒有找到時,應該下載哪一個圖片 - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL; // 容許在圖片已經被下載完成且被緩存到磁盤或內存前當即轉換 - (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL; 

這兩個代理方法會在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中調用,而這個方法是SDWebImageManager類的核心所在。咱們來看看它的具體實現:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                     options:(SDWebImageOptions)options
                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                     completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
  ...
  // 前面省略n行。主要做了以下處理:
  // 1. 判斷url的合法性
  // 2. 建立SDWebImageCombinedOperation對象
  // 3. 查看url是不是以前下載失敗過的
  // 4. 若是url爲nil,或者在不可重試的狀況下是一個下載失敗過的url,則直接返回操做對象並調用完成回調
  operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    ...
    if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
      // 下載
      id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
        if (weakOperation.isCancelled) {
          // 操做被取消,則不作任務事情
        }
        else if (error) {
          // 若是出錯,則調用完成回調,並將url放入下載挫敗url數組中
          ...
        }
        else {
          BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
          if (options & SDWebImageRefreshCached && image && !downloadedImage) {
            // Image refresh hit the NSURLCache cache, do not call the completion block
          }
          else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
            // 在全局隊列中並行處理圖片的緩存
            // 首先對圖片作個轉換操做,該操做是代理對象實現的
            // 而後對圖片作緩存處理
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
              UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
              if (transformedImage && finished) {
                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
              }
              ...
            });
          }
          else {
            if (downloadedImage && finished) {
              [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
            }
            ...
          }
        }
        // 下載完成並緩存後,將操做從隊列中移除
        if (finished) {
          @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
          }
        }
      }];
      // 設置取消回調
      operation.cancelBlock = ^{
        [subOperation cancel];
        @synchronized (self.runningOperations) {
          [self.runningOperations removeObject:weakOperation];
        }
      };
    }
    else if (image) {
      ...
    }
    else {
      ...
    }
  }];
  return operation;
}

 

對於這個方法,咱們沒有作過多的解釋。其主要就是下載圖片並根據操做選項來緩存圖片。上面這個下載方法中的操做選項參數是由枚舉SDWebImageOptions來定義的,這個操做中的一些選項是與SDWebImageDownloaderOptions中的選項對應的。咱們來看看這個SDWebImageOptions選項都有哪些:

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
  // 默認狀況下,當URL下載失敗時,URL會被列入黑名單,致使庫不會再去重試,該標記用於禁用黑名單
  SDWebImageRetryFailed = 1 << 0,
  // 默認狀況下,圖片下載開始於UI交互,該標記禁用這一特性,這樣下載延遲到UIScrollView減速時
  SDWebImageLowPriority = 1 << 1,
  // 該標記禁用磁盤緩存
  SDWebImageCacheMemoryOnly = 1 << 2,
  // 該標記啓用漸進式下載,圖片在下載過程當中是漸漸顯示的,如同瀏覽器一下。
  // 默認狀況下,圖像在下載完成後一次性顯示
  SDWebImageProgressiveDownload = 1 << 3,
  // 即便圖片緩存了,也指望HTTP響應cache control,並在須要的狀況下從遠程刷新圖片。
  // 磁盤緩存將被NSURLCache處理而不是SDWebImage,由於SDWebImage會致使輕微的性能下載。
  // 該標記幫助處理在相同請求URL後面改變的圖片。若是緩存圖片被刷新,則完成block會使用緩存圖片調用一次
  // 而後再用最終圖片調用一次
  SDWebImageRefreshCached = 1 << 4,
  // 在iOS 4+系統中,當程序進入後臺後繼續下載圖片。這將要求系統給予額外的時間讓請求完成
  // 若是後臺任務超時,則操做被取消
  SDWebImageContinueInBackground = 1 << 5,
  // 經過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲在NSHTTPCookieStore中的cookie
  SDWebImageHandleCookies = 1 << 6,
  // 容許不受信任的SSL認證
  SDWebImageAllowInvalidSSLCertificates = 1 << 7,
  // 默認狀況下,圖片下載按入隊的順序來執行。該標記將其移到隊列的前面,
  // 以便圖片能當即下載而不是等到當前隊列被加載
  SDWebImageHighPriority = 1 << 8,
  // 默認狀況下,佔位圖片在加載圖片的同時被加載。該標記延遲佔位圖片的加載直到圖片已以被加載完成
  SDWebImageDelayPlaceholder = 1 << 9,
  // 一般咱們不調用動畫圖片的transformDownloadedImage代理方法,由於大多數轉換代碼能夠管理它。
  // 使用這個票房則不任何狀況下都進行轉換。
  SDWebImageTransformAnimatedImage = 1 << 10,
};

 

你們在看-downloadImageWithURL:options:progress:completed:,能夠看到兩個SDWebImageOptions與SDWebImageDownloaderOptions中的選項是如何對應起來的,在此很少作解釋。

 

SDWebImageManager的幾個方法

(1)- (void)cancelAll   : 取消runningOperations中全部的操做,並所有刪除

(2)- (BOOL)isRunning  :檢查是否有操做在運行,這裏的操做指的是下載和緩存組成的組合操做

(3) - downloadImageWithURL:options:progress:completed:   核心方法

(4)- (BOOL)diskImageExistsForURL:(NSURL *)url  :指定url的圖片是否進行了磁盤緩存

 

4、視圖擴展

我在使用SDWebImage的時候,使用得最多的是UIImageView+WebCache中的針對UIImageView的擴展方法,這些擴展方法將UIImageView與WebCache集成在一塊兒,來讓UIImageView對象擁有異步下載和緩存遠程圖片的能力。其中最核心的方法是-sd_setImageWithURL:placeholderImage:options:progress:completed:,其使用SDWebImageManager單例對象下載並緩存圖片,完成後將圖片賦值給UIImageView對象的image屬性,以使圖片顯示出來,其具體實現以下:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
  ...
  if (url) {
    __weak UIImageView *wself = self;
    // 使用SDWebImageManager單例對象來下載圖片
    id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
      if (!wself) return;
      dispatch_main_sync_safe(^{
        if (!wself) return;
        // 圖片下載完後顯示圖片
        if (image) {
          wself.image = image;
          [wself setNeedsLayout];
        } else {
          if ((options & SDWebImageDelayPlaceholder)) {
            wself.image = placeholder;
            [wself setNeedsLayout];
          }
        }
        if (completedBlock && finished) {
          completedBlock(image, error, cacheType, url);
        }
      });
    }];
    [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
  } else {
    ...
  }
}

 

除了擴展UIImageView以外,SDWebImage還擴展了UIView、UIButton、MKAnnotationView等視圖類,你們能夠參考源碼。

固然,若是不想使用這些擴展,則能夠直接使用SDWebImageManager來下載圖片,這也是很OK的。

 

5、技術點

SDWebImage的主要任務就是圖片的下載和緩存。爲了支持這些操做,它主要使用瞭如下知識點:

  1. dispatch_barrier_sync函數:該方法用於對操做設置順序,確保在執行完任務後纔會執行後續操做。該方法經常使用於確保類的線程安全性操做。

  2. NSMutableURLRequest:用於建立一個網絡請求對象,咱們能夠根據須要來配置請求報頭等信息。

  3. NSOperation及NSOperationQueue:操做隊列是Objective-C中一種高級的併發處理方法,如今它是基於GCD來實現的。相對於GCD來講,操做隊列的優勢是能夠取消在任務處理隊列中的任務,另外在管理操做間的依賴關係方面也容易一些。對SDWebImage中咱們就看到了如何使用依賴將下載順序設置成後進先出的順序。

  4. NSURLConnection:用於網絡請求及響應處理。在iOS7.0後,蘋果推出了一套新的網絡請求接口,即NSURLSession類。

  5. 開啓一個後臺任務。

  6. NSCache類:一個相似於集合的容器。它存儲key-value對,這一點相似於NSDictionary類。咱們一般用使用緩存來臨時存儲短期使用但建立昂貴的對象。重用這些對象能夠優化性能,由於它們的值不須要從新計算。另一方面,這些對象對於程序來講不是緊要的,在內存緊張時會被丟棄。

  7. 清理緩存圖片的策略:特別是最大緩存空間大小的設置。若是全部緩存文件的總大小超過這一大小,則會按照文件最後修改時間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小於咱們設置的最大使用空間。

  8. 對圖片的解壓縮操做:這一操做能夠查看SDWebImageDecoder.m中+decodedImageWithImage方法的實現。

  9. 對GIF圖片的處理

  10. 對WebP圖片的處理

相關文章
相關標籤/搜索