【原】SDWebImage源碼閱讀(一)

【原】SDWebImage源碼閱讀(一)

本文轉載請註明出處 —— polobymulberry-博客園html

1. 前言


一直沒有系統地讀過整套源碼,就感受像一直看零碎的知識點,沒有系統讀過一本專業經典書籍同樣,會有點發虛,感受知識體系不健全!廢話少說,此次我決定好好閱讀下SDWebImage的源碼,個人閱讀方式,是帶着問題去閱讀源碼,而後強迫本身寫博客git

2. SDWebImage是作什麼的?


既然是要帶着問題讀,那麼第一個問題就來了,SDWebImage是作什麼的?SDWebImage是一個開源的代碼庫,咱們能夠在github上找到它 —> Github傳送門github

Github上是這樣介紹它的:web

This library provides a category for UIImageView with support for remote images coming from the web.緩存

因此咱們大概知道SDWebImage就是一個庫。這個庫本質是UIImageView的category。爲啥要作這個category呢?是爲了從服務器端遠程獲取圖片到UIImageView上顯示。固然,看完代碼後,就知道SDWebImage提供的功能遠不止說的這麼簡單。服務器

3. SDWebImage怎麼用?


github上也給了一些例子,咱們看一下最經常使用的一個例子:網絡

 1 #import <SDWebImage/UIImageView+WebCache.h>
 2 
 3 ...
 4 
 5 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 6     static NSString *MyIdentifier = @"MyIdentifier";
 7 
 8     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
 9     if (cell == nil) {
10         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
11                                        reuseIdentifier:MyIdentifier] autorelease];
12     }
13 
14     // Here we use the new provided sd_setImageWithURL: method to load the web image
15     [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
16                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
17 
18     cell.textLabel.text = @"My Text";
19     return cell;
20 }

這確實是一個很常見的需求,就是在一個tableView上,每個cell都須要顯示網絡端獲取的image。好比咱們經常使用的新浪微博、網易新聞、知乎日報等等,都會用到。app

這裏最關鍵的一行代碼就是:dom

1     // Here we use the new provided sd_setImageWithURL: method to load the web image
2     [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
3                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

看到這裏,我不由自主地要讚歎兩句,這個接口設計的真的很棒!你想一想,我要從網絡端獲取圖片,並顯示到UIImageView上,其實我只要給你一個圖片的url就ok啦,另外當前圖片若是還未獲取到,怎麼辦?弄個placeholderImage唄(當網絡端圖片還未加載完成,做爲一個替代的圖片,好比一些app若是網絡很差的話,文章對應圖片加載不出來,就會顯示帶有「暫無圖片」的圖片)。async

其中的圖片如何獲取,如何緩存等等都屏蔽了。甚至沒有暴露從網絡端獲取到的是什麼圖片,固然後面咱們會提到SDWebImage中有其餘的藉口會暴露返回的圖片image,容許在image上操做後再賦值給imageView。

細想下其中的過程,我大致有一個簡單的實現概念(先本身想一想怎麼實現,而後對照實際源碼,這樣才能看到本身不足):

  1. 先將UIImageView的image設爲placeholderImage
  2. 而後發出網絡請求,獲取圖片image

  3. 若是圖片獲取成功,賦值給UIImageView

帶着我這簡陋的想法,我模仿SDWebImage寫了以下代碼:

首先我建立了一個UIImageView的category —— UIImageView+Extension.h

主要是模仿SDWebImage的 sd_setImageWithURL:placeholderImage:函數寫了一個pjx_setImageWithURL:placeholderImage:

- UIImageView+Extension.h

1 #import <UIKit/UIKit.h>
2 
3 @interface UIImageView (Extension)
4 
5 - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage;
6 
7 @end

- UIImageView+Extension.m

 1 #import "UIImageView+Extension.h"
 2 
 3 @implementation UIImageView (Extension)
 4 
 5 - (void)pjx_setImageWithURL:(NSURL *)imageUrl placeholderImage:(UIImage *)placeholderImage
 6 {
 7     // 1.先將UIImageView的image設爲placeholderImage
 8     self.image = placeholderImage;
 9     
10     // 2.而後發出網絡請求,獲取圖片image
11     NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
12     UIImage *image = [UIImage imageWithData:imageData];
13     
14     // 3.若是圖片獲取成功,賦值給UIImageView
15     if (image) {
16         self.image = image;
17     }
18 }
19 
20 @end

ViewController調用代碼:

 1 #import "ViewController.h"
 2 #import "UIImageView+Extension.h"
 3 
 4 @interface ViewController ()
 5 
 6 @property (weak, nonatomic) IBOutlet UIImageView *imageView;
 7 
 8 @end
 9 
10 @implementation ViewController
11 
12 #pragma mark - life cycle
13 - (void)viewDidLoad {
14     [super viewDidLoad];
15     
16     NSString *baiduLogoString = @"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png";
17     
18     [self.imageView pjx_setImageWithURL:[NSURL URLWithString:baiduLogoString] placeholderImage:[UIImage imageNamed:@"placeholderImage"]];
19 }
20 
21 @end

效果以下:

沒有網絡(左)有網絡(右)狀況下的對比圖

 

而後我喜滋滋地去看SDWebImage的sd_setImageWithURL:placeholderImage:實現,結果~~他竟然調用的是另一個巨多參數的函數:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

大概猜下,除了url和placeholder兩個參數懂是什麼意思,options不明白,progress確定表示的是下載的進度,也就是正在下載時候所要處理的事情(block),那麼completed應該表示的是下載完成後所要作的事(block)

因而我定位到了該函數,發現本身徹底不是一個級別上的,看不懂。不過我還不死心,因而我全局搜索dataWithContentsOfURL,嗯,SDWebImage竟然沒有用!好吧,先無論了,不用就不用,那你總得給UIImageView的image賦值吧,並且確定要賦值一次placeholderImage和網絡請求獲得的image吧。果真找到了,就在上面那個巨多參數的函數中。我只截取了部分代碼

 1 ......
 2 if (!(options & SDWebImageDelayPlaceholder)) {
 3     dispatch_main_async_safe(^{
 4         self.image = placeholder;
 5     });
 6 }
 7 .....
 8 else if (image) {
 9     wself.image = image;
10     [wself setNeedsLayout];
11 } else {
12     if ((options & SDWebImageDelayPlaceholder)) {
13         wself.image = placeholder;
14         [wself setNeedsLayout];
15     }
16 }
17 ...

能夠看到這裏實現了三處image的賦值。而且後面兩處賦值後當即使用setNeedsLayout來進行刷新(我註釋了刷新代碼,好像沒有發生什麼問題,不過這裏仍是注意一下,確定是某個情形下會發生沒法自動刷新圖片的狀況,纔要手動刷新)。好的,這裏咱們能夠停一下,看看這些image賦值都是發生在什麼狀況下的。

這幾處賦值都出現了SDWebImageDelayPlaceholder。看下它的註釋,首先,它是一個SDWebImageOptions枚舉值,而參數options也是一個枚舉類型的變量,註定二者是好基友了。話說回來,SDWebImageDelayPlaceholder表示的是什麼呢?看註釋:

    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */

翻譯過來就是,默認狀況下,當正在加載網絡端的image 時,placeholder已經加載到了UIImageView,這個枚舉項就是爲了不這種默認狀況,他將延遲placeholder的加載直到網絡端的image加載完成。可能有些抽象,看代碼就好了。

在還沒發送請求獲取網絡端圖片以前(即網絡端的image還沒加載),若是options中有SDWebImageDelayPlaceholder這一選項,就不給image賦值,若是沒有這一項,那麼就給image賦值placeholder。說白了就是下面這段代碼:

1 if (!(options & SDWebImageDelayPlaceholder)) {
2     dispatch_main_async_safe(^{
3         self.image = placeholder;
4     });
5 }

其中dispatch_main_async_safe就是SDWebImage定義的一個宏,很好理解:若是當前是主進程,就直接執行block,不然把block放到主進程運行。爲何要判斷是不是主進程?由於iOS上任何UI的操做都在主線程上執行,因此主進程還有一個名字,叫作「UI進程」。

1 #define dispatch_main_async_safe(block)\
2     if ([NSThread isMainThread]) {\
3         block();\
4     } else {\
5         dispatch_async(dispatch_get_main_queue(), block);\
6     }

後面咱們看到有一處代碼,表示即便options中有SDWebImageDelayPlaceholder這一選項,也給image賦值placeholder,爲啥了?由於此時image已經從網絡端加載過了,可是網絡端獲取image沒成功,此時纔會用placeholder來替代,赤裸裸的備胎,有代碼爲證。

else { // image已經嘗試獲取過了,可是沒有從網絡端獲取到
    if ((options & SDWebImageDelayPlaceholder)) {
        wself.image = placeholder;
        [wself setNeedsLayout];
    }
}

而else上面的else if那段代碼,就是表示image從網絡獲取成功,直接賦值給image。

哈哈,不知道大家會不會有疑惑,你怎麼知道此處表示向網絡獲取image的,也就是註釋中說的the image is loading?~~我猜的,不過我猜的沒錯的話,這段獲取的代碼既然總體賦值給了id <SDWebImageOperation> operation,那多是爲了多任務(多個圖片加載),爲何呢?我懷疑SDWebImageOperation是一個NSOperation子類(這樣才能放到NSOperationQueue中進行多任務嘛)。大家確定說我是SB,這一看就是一個protocol嘛!確實是我猜錯了,可是我隱約以爲既然叫Operation,不跟NSOperation有點關係也說不清啊,或者它可能模仿了NSOperation的多任務運行方式。以上都是猜想,咱們仍是來看代碼(後面會揭祕)。

以上的代碼(還有幾處沒說,可是涉及到什麼SDImageCacheType還有其餘的,暫時不去想)封裝成的operation做爲參數放到了sd_setImageLoadOperation中。咱們接着跳到sd_setImageLoadOperation函數中。很簡單,只有三行,我直接貼代碼了:

1 - (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
2     [self sd_cancelImageLoadOperationWithKey:key];
3     NSMutableDictionary *operationDictionary = [self operationDictionary];
4     [operationDictionary setObject:operation forKey:key];
5 }

雖然不少變量和函數不認識,可是咱們大概也能猜到這三行作了什麼。我先看[self operationDictionary],具體定義不要看,咱們知道它是一個NSMutableDictionary便可,並且既然叫operationDictionary,那麼存放的必定是各類operation的序列了(固然也就包括SDWebImageOperation類型的operation),並且這些operation是根據key來索引的。好的,咱們回到函數中。一進函數,先取消索引爲key的operation的操做,有些人說,若是我以前正在進行索引爲key的操做,那不就取消了嘛?是啊,就是這樣,若是該operation存在,就取消掉了,還要刪除這個key對應的object(operation)。而後從新設置key對應的operation。咱們能夠看看函數sd_cancelImageLoadOperationWithKey。(這一段文字我解釋得很差,下面評論區有詳細解釋)

 1 - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
 2     // Cancel in progress downloader from queue
 3     NSMutableDictionary *operationDictionary = [self operationDictionary];
 4     id operations = [operationDictionary objectForKey:key];
 5     if (operations) {
 6         if ([operations isKindOfClass:[NSArray class]]) {
 7             for (id <SDWebImageOperation> operation in operations) {
 8                 if (operation) {
 9                     [operation cancel];
10                 }
11             }
12         } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
13             [(id<SDWebImageOperation>) operations cancel];
14         }
15         [operationDictionary removeObjectForKey:key];
16     }
17 }

代碼也很容易理解,先獲取到operation的序列,即[self operationDictionary]。而後根據key來索引到對應的operation,若是operation存在的話。就要取消該operation。這裏有一個注意的地方,也是我以前沒想到的,就是索引到的operation其實一組operation的集合,那麼就須要來個遍歷一個個取消掉operation序列中的operation了。最後移除key對應的object。

這裏我有個疑惑:爲啥operation都是id<SDWebImageOperation>?並且,大家也注意到了SDWebImageOperation只有一個cancel接口。爲何要這樣設計,還有待進一步研究。

咱們仍是回到sd_setImageWithURL這個函數中,如今咱們有個大概思路了。咱們來看看咱們可以理解的部分:

未完待續,請君移步【原】SDWebImage源碼閱讀(二)

相關文章
相關標籤/搜索