SDWebImage 解析筆記

項目中一直都有使用SDWebImage,對這個框架有一定的了解,但是體系卻未能貫通,因此特地整理下,主要參考:

iOS 源代碼分析 --- SDWebImage

SDWebImage源碼剖析(-)

SDWebImage源碼剖析(二)

蝶.jpg

一. 簡介

SDWebImage提供了一個異步下載圖片并且支持緩存的UIImageView分類。
主要邏輯為:

  • 查看緩存,如果緩存中存在圖片就返回圖片并且更新UIImageView.
  • 緩存中不存在圖片就異步下載圖片,加入緩存,更新UIImageView.

主要用到的對象:

1、UIImageView (WebCache)類別,入口封裝,實現讀取圖片完成后的回調

2、SDWebImageManager,對圖片進行管理的中轉站,記錄那些圖片正在讀取。
向下層讀取Cache(調用SDImageCache),或者向網絡請求下載對象(調用SDWebImageDownloader) 。
實現SDImageCacheSDWebImageDownloader的回調。

3、SDImageCache,根據URL的MD5生成key對圖片進行存儲和讀取(實現存在內存中或者存在硬盤上兩種實現)
實現圖片和內存清理工作。

4、SDWebImageDownloader,根據URL向網絡讀取數據(實現部分讀取和全部讀取后再通知回調兩種方式)

其他類:
SDWebImageDecoder,異步對圖像進行了一次解壓。

具體流程圖:


SDWebImage實現流程圖.png

SDWebImage 加載圖片的流程 :

  1. 入口 setImageWithURL:placeholderImage:options:會先把placeholderImage顯示,然后 SDWebImageManager 根據 URL 開始處理圖片。

  2. 進入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交給 SDImageCache從緩存查找圖片是否已經下載 queryDiskCacheForKey:delegate:userInfo:.

  3. 先從內存圖片緩存查找是否有圖片,如果內存中已經有圖片緩存,SDImageCacheDelegate回調 imageCache:didFindImage:forKey:userInfo:SDWebImageManager

4.SDWebImageManagerDelegate回調 webImageManager:didFinishWithImage:UIImageView+WebCache 等前端展示圖片。

  1. 如果內存緩存中沒有,生成NSInvocationOperation 添加到隊列開始從硬盤異步查找圖片是否已經緩存。

  2. 根據 URLKey 在硬盤緩存目錄下嘗試讀取圖片文件。這一步是在 NSOperation 進行的操作,所以回主線程進行結果回調 notifyDelegate:

  3. 如果上一操作從硬盤讀取到了圖片,將圖片添加到內存緩存中(如果空閑內存過小,會先清空內存緩存)。SDImageCacheDelegate回調imageCache:didFindImage:forKey:userInfo:。進而回調展示圖片。

  4. 如果從硬盤緩存目錄讀取不到圖片,說明所有緩存都不存在該圖片,需要下載圖片,回調 imageCache:didNotFindImageForKey:userInfo:

  5. 共享或重新生成一個下載器 SDWebImageDownloader 開始下載圖片。

  6. 圖片下載由 NSURLConnection 來做,實現相關 delegate 來判斷圖片下載中、下載完成和下載失敗。

  7. connection:didReceiveData: 中利用ImageIO 做了按圖片下載進度加載效果。

  8. connectionDidFinishLoading:數據下載完成后交給 SDWebImageDecoder做圖片解碼處理。

  9. 圖片解碼處理在一個 NSOperationQueue 完成,不會拖慢主線程 UI。如果有需要對下載的圖片進行二次處理,最好也在這里完成,效率會好很多。

  10. 在主線程 notifyDelegateOnMainThreadWithInfo:宣告解碼完成,imageDecoder:didFinishDecodingImage:userInfo:回調給 SDWebImageDownloader

  11. imageDownloader:didFinishWithImage: 回調給 SDWebImageManager 告知圖片下載完成。

  12. 通知所有的 downloadDelegates 下載完成,回調給需要的地方展示圖片。

  13. 將圖片保存到 SDImageCache 中,內存緩存和硬盤緩存同時保存。寫文件到硬盤也在以單獨 NSInvocationOperation 完成,避免拖慢主線程。

  14. SDImageCache 在初始化的時候會注冊一些消息通知,在內存警告或退到后臺的時候清理內存圖片緩存,應用結束的時候清理過期圖片。

  15. SDWebImage 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

  16. SDWebImagePrefetcher 可以預先下載圖片,方便后續使用。

二. 架構簡介

A.架構圖:

SDWebImageView_relationship.jpeg

UIImageView+WebCacehUIButton+WebCache直接為UIkit框架提供接口,而SDWebImageManger負責處理和協調SDWebImageDownloaderSDWebImageCache并與UIkit層進行交互。

三. 具體分析

1.UIImageView+WebCache

A.框架常用入口

// 所有設置圖片最終都會調用這個方法
- (void)sd_setImageWithURL:(NSURL *)url 
      placeholderImage:(UIImage *)placeholder {
  [self sd_setImageWithURL:url 
        placeholderImage:placeholder 
                 options:0 
                progress:nil 
               completed:nil];
  }

該接口調用下面這個方法:

[self   sd_setImageWithURL:placeholderImage:options:progress:completed:]

該方法作為sd_setImageWithURL接口的最終入口,提供了多種參數。

  • url:遠程圖片的地址

  • placeholder : 預顯示圖片

  • optionsSDWebImageOptions

      typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { 
      //下載失敗了會再次嘗試下載 
      SDWebImageRetryFailed = 1 << 0,
    
      //當UIScrollView等正在滾動時,延遲下載圖片(放置scrollView滾動卡)     
      WebImageLowPriority = 1 << 1,
    
      //只緩存到內存中
      SDWebImageCacheMemoryOnly = 1 << 2, 
    
      // 圖片會邊下邊顯示
      SDWebImageProgressiveDownload = 1 << 3, 
    
     // 將硬盤緩存交給系統自帶的NSURLCache去處理 
      SDWebImageRefreshCached = 1 << 4,
    
     //后臺下載 
      SDWebImageContinueInBackground = 1 << 5,
    
      // 通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲在NSHTTPCookieStore中的cookie 
      SDWebImageHandleCookies = 1 << 6,
    
      // 允許不受信任的SSL證書。主要用于測試目的。 
      SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    
      // 默認情況下,image在裝載的時候是按照他們在隊列中的順序裝載的(就是先進先出).這個flag會把他們移動到隊列的前端,并且立刻裝載,而不是等到當前隊列裝載的時候再裝載
      SDWebImageHighPriority = 1 << 8,   
    
      // 默認情況下,占位圖會在圖片下載的時候顯示.這個flag開啟會延遲占位圖顯示的時間,等到圖片下載完成之后才會顯示占位圖
      SDWebImageDelayPlaceholder = 1 << 9, 
    
       // 是否transform圖片
      SDWebImageTransformAnimatedImage = 1 << 10,
      };
    
  • progress :下載進度

B.代碼分析:

操作的管理:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
     
    // 取消當前下載操作
    [self sd_cancelCurrentImageLoad];

    // 動態添加屬性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 如果選項非SDWebImageDelayPlaceholder
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            // 設置占位圖
            self.image = placeholder;
        });
    }



    if (url.absoluteString.length > 0) {

        // check if activityView is enabled or not
        if ([self showActivityIndicatorView]) {
             // 顯示 下載轉圈
            [self addActivityIndicator];
       }

        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            // 下載完成回調
            // 移除下載進度轉圈
            [wself removeActivityIndicator];
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else 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 {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
       });
    }
}

[self sd_cancelCurrentImageLoad];取消當前的下載操作,它表明 SDWebImage 管理操作的方法:
SDWebImage所有的操作實際都是通過一個 operationDictionary 的字典管理,這個字典是動態添加到 UIView 上的一個屬性,因為這個operationDictionary 需要在UIButtonUIImageView 上重用,所以需要添加到它們的根類上。

這行代碼是要保證沒有當前正在進行的異步下載操作, 不會與即將進行的操作發生沖突, 它會調用:

// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self  sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]

這行代碼會取消當前這個UIImageView的所有操作,不會影響之后進行的下載操作。

占位圖的實現:

// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;
}

options中沒有SDWebImageDelayPlaceholder,UIImageView添加一個占位圖image.

獲取圖片:

 // UIImageView+WebCache
 // sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)

檢測傳入的URL是否為空,如果非空就調用全局的SDWebImageManager來獲取圖片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

下載完成后調用(SDWebImageCompletionWithFinishedBlock)completedBlock 為 UIImageView.image 賦值, 添加上最終所需要的圖片.

// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #10

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); 
  }
});

最后在返回 operation的同時, 也會向 operationDictionary中添加一個鍵值對, 來表示操作的正在進行:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

它將operation 存儲到operationDictionary 中方便以后的cancel操作。

清晨.jpg

2. SDWebImageManager

這個類主要用于處理異步下載和圖片緩存的類,也可以直接用SDWebImageManagerdownloadImageWithURL:options:progress:completed:來直接下載圖片。
可以看出這個類主要作用就是為了UIImageView+WebCacheSDWebImageDownloader, SDImageCache之間構建一個橋梁,使它們能夠更好的協同工作。

A.核心代碼分析:

a.SDWebImageManager

// SDWebImageManager
//- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:

if ([url isKindOfClass:NSString.class]) { 
  url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) { 
  url = nil;
}

這塊代碼的功能是確定 url是否被正確傳入, 如果傳入參數的是 NSString類型就會被轉換為NSURL, 如果轉換失敗, 那么url會被賦值為空, 這個下載的操作就會出錯.

b. SDWebImageCombinedOperation

url被正確傳入之后, 會實例一個非常奇怪的 operation, 它其實是一個遵循 SDWebImageOperation
協議的 NSObject的子類. 而這個協議也非常的簡單:

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

SDWebImageOperation只是看著像NSOperation但是它唯一跟NSOperation相同就是都可以響應cancel方法。調用這個類的cancel方法,會使得它持有的兩個operation都被cancel

// SDWebImageCombinedOperation
// cancel #1
- (void)cancel { 
      self.cancelled = YES; 
      if (self.cacheOperation) { 
            [self.cacheOperation cancel]; 
            self.cacheOperation = nil; 
      } 
      if (self.cancelBlock) {
           self.cancelBlock(); 
          _cancelBlock = nil; 
      }
  }

既然獲取了url,再通過url獲取對應的key.

NSString *key = [self cacheKeyForURL:url];

接著通過key在緩存中查找一起是否下載過相同的圖片

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }];

這里調用SDImageCache的實例方法 queryDiskCacheForKey:done:來嘗試在緩存中獲取圖片的數據,而這個方法獲取的就是貨真價實的NSOperation.
如果我們在緩存中查找到對應的圖片,那么我們直接調用completedBlock回調塊結束這一次圖片的下載操作

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});

如果沒有找到就調用SDWebImageDownLoader的實例方法去下載該圖片:

id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];

如果這個方法返回正確的downloadedImage ,那么我們就在全局緩存中存儲這個圖片的數據:

 [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];

并調用completedBlockUIImageView或者UIButton添加圖片。

最后我們將這個subOperationcancel 操作添加到operation.cancelBlock中,方便操作的取消

operation.cancelBlock = ^{ [subOperation cancel]; }

3. SDWebImageCache

維護了一個內存緩存和一個可選的磁盤緩存,首先看下查詢圖片緩存的方法:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

該方法主要功能是異步查詢圖片緩存,先在內存中查找

// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];

// 內存中查找圖片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];

}

imageFromMemoryCacheForKey:key 方法會在SDWebImageCache 維護的緩存memCache 中查找是否有對應的數據,而 memCache 就是一個 NSCache.

NSCache 是一個類似于 NSMutableDictionary 存儲 key-value 的容器,主要有以下幾個特點:

自動刪除機制:當系統內存緊張時,NSCache會自動刪除一些緩存對象
線程安全:從不同線程中對同一個 NSCache 對象進行增刪改查時,不需要加鎖
不同于 NSMutableDictionaryNSCache存儲對象時不會對key進行 copy 操作

如果在內存中沒有找到圖片的緩存的話,就需要在磁盤中查找。

- (UIImage *)diskImageForKey:(NSString *)key {
   NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; 
   if (data) { 
      UIImage *image = [UIImage sd_imageWithData:data];
       image = [self scaledImageForKey:key image:image];
       if (self.shouldDecompressImages) {
           image = [UIImage decodedImageWithImage:image];
          } 
      return image; 
  }
 else { 
  return nil; 
  }
}

得到圖片對應的NSData后還有經過:

  • 根據圖片的不同種類,生成對應的UIImage,
  • 根據key值,調整imageScale
  • 如果設置圖片需要解壓縮,需要對圖片進行解碼

對圖片進行存儲需要對url進行MD5加密計算生成相應的key值:

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                      r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                      r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

然后用該key作為圖片文件名存儲在默認路徑下:

// 獲取緩存路徑方法(自己寫的)
- (NSString*)getCachePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
NSString *path = [paths[0] stringByAppendingFormat:@"/com.hackemist.SDWebImageCache.default"];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}else{
return nil;
}
}

之前做朋友圈后臺發送圖片就是先將小圖命名,然后根據獲取到的七牛的domain和token,拼出url,接著將該url,進行md5加密,加密后存儲到SDWebImage的默認存儲路徑下,然后在主界面顯示存儲的小圖,后臺去進行圖片壓縮上傳任務。

 UIImage *diskImage = [self diskImageForKey:key];
  if (diskImage && self.shouldCacheImagesInMemory) {
       NSUInteger cost = SDCacheCostForImage(diskImage);
       [self.memCache setObject:diskImage forKey:key cost:cost];
   }



如果在磁盤中找到圖片,就將他復制到內存中,以便下次使用。

樹.jpg

4.SDWebImageDownloader

專用的并且優化的圖片異步下載器,主要用來下載圖片,下載放在NSOperationQueue中進行,默認maxConcurrentOperationCount為6,timeout時間為15s.

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

該方法直接調用了下載進度回調函數:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

方法會先查看這個 url是否有對應的 callback, 使用的是 downloader,持有的一個字典URLCallbacks.
如果是第一次添加回調的話, 就會執行first = YES, 這個賦值非常的關鍵, 因為 first不為 YES那么 HTTP 請求就不會被初始化, 圖片也無法被獲取.
然后, 在這個方法中會重新修正在URLCallbacks中存儲的回調塊.

通過dispatch_barrier_async函數提交的任務會等它前面的任務執行完才開始,然后它后面的任務必須等它執行完畢才能開始. 必須使用dispatch_queue_create創建的隊列才會達到上面的效果.通過該函數來保證每張圖片進度順序。

如果是第一次添加回調塊,那么就會直接運行這個createCallBack這個block,而這個block,就是我們在downloadImageWithURL:options:progress:completed: 中傳入的回調塊.

接著分析下NSMutableURLRequest請求:

 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

request發送了一個http請求,接著又初始化一個SDWebImageDownloaderOperation實例,這個實例用于請求網絡資源的操作,是NSOperation的子類:

operation = [[wself.operationClass alloc] initWithRequest:request
                                                      options:options
                                                     progress:^(NSInteger receivedSize, NSInteger expectedSize) {

初始化之后,將該operation添加到NSOperationQueue中。(備注:NSOperation實例只有在調用start方法或者加入NSOperationQueue 才會執行)

[wself.downloadQueue addOperation:operation];

5.SDWebImageDownloaderOperation

這個類主要處理HTTP請求,URL連接的類,當這個類的實例被加入到隊列之后,start方法被調用,start方法首先產生一個NSURLConnection,通過NSURLConnection進行圖片的下載,為了確保能夠處理下載的數據,需要在后臺運行runloop,保證程序不被掛起.
- (void)start {
@synchronized (self) {
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)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

            if (sself) {
                    [sself cancel];
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#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);
        }

        //在主線程發通知,這樣也保證在主線程收到通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });

        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
           // Make sure to run the runloop in our background thread so it can process downloaded data
            // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
            //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }

        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

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

接下來這個 connection 就會開始運行:

[self.connection start];

它發出一個SDWebImageDownloadStartNotification通知,開啟狀態欄的請求加載轉圈。同時調用NSURLConnectionDataDelegate代理

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

前兩個代理會不停的回調 pregressBlock 來提示下載進度。

而最后一個代理方法會在圖片下載完成之后調用completionBlock 來完成最后 UIImageView.image的更新,而這里調用的 progressBlockcompletionBlockcancelBlock都是在之前存儲在 URLCallbacks
字典中的.

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
    @synchronized(self) {
        // 停止 該線程 運行時
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.thread = nil;
        self.connection = nil;
        // 通知停止狀態欄轉圈請求
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
        });
    }

    if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
        responseFromCached = NO;
    }

    if (completionBlock) {
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
            completionBlock(nil, nil, nil, 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) {
                    image = [UIImage decodedImageWithImage:image];
                }
            }
            if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
            }
            else {
                completionBlock(image, self.imageData, nil, YES);
            }
        } else {
            completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
        }
    }
    self.completionBlock = nil;
    [self done];
}

轉換處理圖片和進行緩存后,將下載image賦值給控件。

四. 面試點

1、SDImageCache是怎么做數據管理的?

  • SDImageCache分成兩部分,一個是內存層面的,一個是磁盤層面的。

  • 內存緩存的處理是使用NSCache對象來實現的。NSCache是一個類似于集合的容器。它存儲key-value對,這一點類似于NSDictionary類,用搜索文件系統的方式做管理,文件替換方式是以時間為單位。我們通常用使用緩存來臨時存儲短時間使用但創建昂貴的對象。重用這些對象可以優化性能,因為它們的值不需要重新計算。另外一方面,這些對象對于程序來說不是緊要的,在內存緊張時會被丟棄。

  • 磁盤緩存的處理則是使用NSFileManager對象來實現的。圖片存儲的位置是位于Cache文件夾,文件替換方式是以時間為單位,剔除時間大一一周的圖片文件。

  • SDWebImageManagerSDImageCache 要資源時, 先搜索內存層面的數據,如果有直接返回,沒有再訪問磁盤,如果有將圖片從磁盤讀取出來,然后做解壓,將圖片對象放到內存層面做備份,再返回調用層。

  1. 為什么圖片要進行解壓?
  • 因為UIImageimageWithData函數是每次畫圖的時候才將Data解壓成ARGB圖像,所以在每次畫圖的時候,會有一個解壓操作,這樣效率很低,但是只有瞬時的內存需求,為了提高效率,通過SDWebImageDecoder將包裝在Data下的資源解壓,然后畫在另外一張圖片上面,這樣這張圖片就不需要重復解壓了,這種做法就是典型的空間換取時間的做法。

3.SDWebImage 在多線程下載圖片時防止錯亂的策略

  • SDWebImage 會將ImageView 對象關聯一個下載列表(列表是給AnimationImages用的,這個時候會下載多張圖片),當tableView滾動時,imageView會重設數據源url,這時會cancel掉下載列表中當前對應的下載任務,然后開啟一個新的下載任務,這樣就保證只有當前可見的cell對象的ImageView對象關聯的下載任務能夠回調,不會發生Image錯亂。

  • 同時,SDWebImage 管理了一個全局下載隊列SDWebDownloadManager,并發量設置為6,也就表示如果cell的數目大于6,就會有部分下載隊列處于等待狀態,而且,在添加下載任務到全局的下載隊列中去的時候,SDWebImage 默認采取的是LIFO(后進先出)策略,具體是添加新的下載任務的時候,將之前的下載任務添加依賴為新的下載任務。

另外解決方案:

  • imageView對象和圖片的url相關聯,在滑動時,不取消舊的下載任務,而是在下載任務完成回調時,進行url匹配,只有匹配成功的image會刷新imageView對象,而其他的image則只做緩存操作,而不刷新UI

  • 同時,仍然管理一個執行隊列,為了避免占用太多的資源,通常會對執行隊列設置一個最大的并發量。此外,為了保證LIFO的下載策略,可以自己維持一個等待隊列,每次下載任務開始的時候,將后進入的下載任務插入到等待隊列的前面。

  1. SDWebImage的主要任務就是圖片的下載和緩存。為了支持這些操作,它主要使用了以下知識點:
  • dispatch_barrier_sync函數:該方法用于對操作設置屏障,確保在執行完任務后才會執行后續操作。該方法常用于確保類的線程安全性操作。

  • NSMutableURLRequest:用于創建一個網絡請求對象,我們可以根據需要來配置請求報頭等信息。

NSOperationNSOperationQueue:操作隊列是Objective-C中一種高級的并發處理方法,現在它是基于GCD來實現的。相對于GCD來說,操作隊列的優點是可以取消在任務處理隊列中的任務,另外在管理操作間的依賴關系方面也容易一些。對SDWebImage中我們就看到了如何使用依賴將下載順序設置成后進先出的順序。

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

開啟一個后臺任務。

  • NSCache類:一個類似于集合的容器。它存儲key-value對,這一點類似于NSDictionary類。我們通常用使用緩存來臨時存儲短時間使用但創建昂貴的對象。重用這些對象可以優化性能,因為它們的值不需要重新計算。另外一方面,這些對象對于程序來說不是緊要的,在內存緊張時會被丟棄。

  • 清理緩存圖片的策略:特別是最大緩存空間大小的設置。如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小于我們設置的最大使用空間。

  • 對圖片的解壓縮操作:這一操作可以查看SDWebImageDecoder.m+decodedImageWithImage方法的實現。

  • GIF圖片的處理

  • WebP圖片的處理

  1. 系統級內存警告如何處理
  • 取消當前正在進行的所有下載操作[[SDWebImageManager sharedManager] cancelAll];
  • 清除緩存數據:
    內存緩存:直接刪除文件,重新創建新的文件
    磁盤緩存:刪除過期的文件數據,計算當前未過期的已經下載的文件數據的大小,如果發現該數據大小大于我們設置的最大緩存數據大小,那么程序內部會按照按文件數據緩存的時間從遠到近刪除,知道小于最大緩存數據為止。
  1. 如何播放gif圖片
  • 把用戶傳入的gif圖片->NSData
  • 根據該Data創建一個圖片數據源(NSData->CFImageSourceRef
  • 計算該數據源中一共有多少幀,把每一幀數據取出來放到圖片數組中
  • 根據得到的數組+計算的動畫時間-》可動畫的image
    • [UIImage animatedImageWithImages:images duration:duration];
  1. 如何判斷當前圖片類型
    + (NSString *)sd_contentTypeForImageData:(NSData *)data;
    圖片的十六進制數據, 的前8個字節都是一樣的, 所以可以通過判斷十六進制來判斷圖片的類型

五. 最后

送上一張自己喜歡的圖片:

風景.jpeg

個人小結,有興趣的朋友可以看一下,如果覺得不錯,麻煩給個喜歡或star,若發現問題請及時反饋,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容