SDWebImage 源碼閱讀筆記(一)


簡介

Asynchronous image downloader with cache support as a UIImageView category.

言簡意賅:SDWebImage 以 UIImageView category(分類)的形式,來支持圖片的異步下載與緩存。

其提供了以下功能:

  1. 以 UIImageView 的分類,來支持網絡圖片的加載與緩存管理
  2. 一個異步的圖片加載器
  3. 一個異步的內存 + 磁盤圖片緩存
  4. 支持 GIF
  5. 支持 WebP
  6. 後臺圖片解壓縮處理
  7. 確保同一個 URL 的圖片不被多次下載
  8. 確保虛假的 URL 不會被反覆加載
  9. 確保下載及緩存時,主線程不被阻塞
  10. 使用 GCD 與 ARC
  11. 支持 Arm64

UIImageView+WebCache

首先,SDWebImage 最常見的使用場景想必如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <SDWebImage/UIImageView+WebCache.h>

...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *MyIdentifier = @"MyIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:MyIdentifier] autorelease];
    }

    // 在這裏,我們使用 UIImageView 分類提供的 sd_setImageWithURL: 方法來加載網絡圖片
    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

    cell.textLabel.text = @"My Text";
    return cell;
}

我們在使用 UITableView 時,往往需要在 Cell 上顯示來自網絡的圖片,這裏最關鍵的一行代碼便是:

1
2
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

於是我們「CMD + 左鍵」來到了 UIImageView+WebCache 查看具體的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * 根據 url、placeholder 與 custom options 爲 imageview 設置 image
 *
 * 下載是異步的,並且被緩存的
 *
 * @param url            網絡圖片的 url 地址
 * @param placeholder    用於預顯示的圖片
 * @param options        一些定製化選項
 * @param progressBlock  下載時的 Block,其定義爲:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
 * @param completedBlock 下載完成時的 Block,其定義爲:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
 */
- (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);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    
    if (url) {
        __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) {
            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(^{
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            if (completedBlock) {
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

雖然代碼只有幾十行,但其中涉及到的知識點卻可不少哦,不要急,讓我們將迷霧一層層剝開:


UIView+WebCacheOperation

首先來看:

1
[self sd_cancelCurrentImageLoad];

「CMD + 左鍵」後帶我們來到了 UIView+WebCacheOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // 取消正在進行的下載隊列
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

框架中的所有操作實際上都是通過一個 operationDictionary(具體查看 UIView+WebCacheOperation)來管理的,而這個 Dictionary 實際上是通過動態的方式(詳情可參見:Objective-C Associated Objects 的實現原理)添加到 UIView 上的一個屬性,至於爲什麼添加到 UIView 上, 主要是因爲這個 operationDictionary 需要在 UIButton 和 UIImageView 上重用,所以需要添加到它們的根類上。

當執行 sd_setImageWithURL: 函數時,首先會 cancel 掉 operationDictionary 中已經存在的 operation,並重新創建一個新的 SDWebImageCombinedOperation 對象來獲取 image,該 operation 會被存入 operationDictionary 中。

這樣來保證每個 UIImageView 對象中永遠只存在一個 operation,當前只允許一個圖片網絡請求,該 operation 負責從緩存中獲取 image 或者是重新下載 image。

SDWebImageCombinedOperation 的 cancel 操作同時會 cacel 掉緩存查詢的 operation 以及 downloader 的 operation


dispatch_main_sync_safe & dispatch_main_async_safe 宏定義

再來看:

1
2
3
dispatch_main_async_safe(^{
           self.image = placeholder;
       });

上述代碼中的 dispatch_main_sync_safedispatch_main_async_safe 均爲宏定義, 點進去一看發現宏是這樣定義的:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

相信你通過這兩個宏的名字就能猜到它們的作用了: 因爲圖像的繪製只能在主線程完成,所以dispatch_main_sync_safedispatch_main_async_safe 就是爲了保證 block 能在主線程中執行。


SDWebImageManager

SDWebImageManager.h 中你可以看到關於 SDWebImageManager 的描述:

The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.

這個類就是隱藏在 UIImageView+WebCache 背後,用於處理異步下載和圖片緩存的類,當然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 來直接下載圖片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
 * 如果在緩存中則直接返回,否則根據所給的 URL 下載圖片
 * 
 * @param url            網絡圖片的 url 地址
 * @param options        一些定製化選項
 * @param progressBlock  下載時的 Block,其定義爲:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
 * @param completedBlock 下載完成時的 Block,其定義爲:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
 * @return 				 返回 SDWebImageOperation 的實例
 */
- (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,則直接返回操作對象並調用完成回調 
    */
    // 根據 URL 生成對應的 key,沒有特殊處理爲 [url absoluteString];
    NSString *key = [self cacheKeyForURL:url];
    // 去緩存中查找圖片(參見 SDImageCache)
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) 
    {
       /* ... */
       // 如果在緩存中沒有找到圖片,或者採用的 SDWebImageRefreshCached 選項,則從網絡下載
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                dispatch_main_sync_safe(^{
                  // 如果圖片找到了,但是採用的 SDWebImageRefreshCached 選項,通知獲取到了圖片,並再次從網絡下載,使 NSURLCache 重新刷新
                     completedBlock(image, nil, cacheType, YES, url);
                });
            }
            /* 下載選項設置 */ 
            // 使用 imageDownloader 開啓網絡下載
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                /* ... */
               if (downloadedImage && finished) {
                     // 下載完成後,先將圖片保存到緩存中,然後主線程返回
                     [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                     dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
          /* ... */
       }
        else if (image) {
          // 在緩存中找到圖片了,直接返回
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
        }
    }];
    return operation;
}

更詳細的註解可參見:SDWebImage源碼解析之SDWebImageManager的註解

要點

  1. 在 SDWebImageManager 中管理了一個 failedURLs 的 NSMutableSet,裏面下載失敗的 url 會被存儲下來。同時,可以通過 SDWebImageRetryFailed 來強制繼續重試下載

  2. 查找緩存,若緩存中沒有 image 則通過 SDWebImageDownloader 來進行下載,下載完成後通過 SDImageCache 進行緩存,會同時緩存到 memCache 和 diskCache 中


可以看到 SDWebImageManager 這個類的主要作用就是爲 UIImageView+WebCache 和 SDWebImageDownloader,SDImageCache 之間構建一個橋樑,使它們能夠更好的協同工作,在接下來的系列文章中,就讓我們一探究竟:它是如何協調異步下載和圖片緩存的?


原文:http://itangqi.me/2016/03/19/the-notes-of-learning-sdwebimage-one/


發佈了24 篇原創文章 · 獲贊 19 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章