SDWebImage 源碼分析

SDWebImage 源碼分析

首先我 fork 了 SDWebImage 的源碼,見 conintet/SDWebImage,這樣在本文的連接中都是鏈到個人 fork 中,這麼作的目的是防止未來 SDWebImage 代碼發生變化致使本文的連接不許確。html

有關 SD (SDWebImage 簡稱爲 SD) 的使用方式仍是得參考其 README 或者 wiki。本文只是閱讀其源碼的筆記。ios

圖片下載

最早分析的就是圖片下載部分的代碼,由於這是最核心的功能。git

由於 SD 在 UIImageView 上經過 Category 的方式增長了簡單易用的 API,相似下面:github

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
複製代碼

因而經過幾步 Jump to Definition 就能夠發現,SD 的圖片下載操做是由 SDWebImageDownloaderOperation 來完成的,因而看一下它的初始化方法:objective-c

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;
複製代碼

經過上面的方法簽名,能夠大概反向的知道:shell

  1. 使用了 NSURLRequest,那麼極可能內部就使用的 NSURLConnection 來完成的下載
  2. 既然提供了 progresscompleted 這兩個 callback,那麼內部勢必須要知道下載的進度
  3. 由於提供了 cancelled 這個 callback,那麼內部的下載操做還須要能夠取消

再看一下 SDWebImageDownloaderOperation 是繼承於 NSOperation,由於下載是一個能夠獨立出來的計算單元,因此做爲 Opreation 是很好理解的。而後在實際的圖片下載中,爲了下載的效率,下載的 Opreations 之間確定是須要併發的。Operation 默認在其被調用的線程中是同步執行的,不過因爲 Operation Queue 的存在,它能夠將其中的 Operations 分別 attach 到由系統控制的線程中,而這些由系統控制的線程之間是併發執行的。緩存

查看 SDWebImageDownloaderOperation 源碼發現內部果真是使用的 NSURLConnection,那麼因爲須要提供 cancelled 的功能以及須要監聽下載進度,故必須將 NSURLConnection 的實例配置成異步的方式:安全

具體代碼在 L96bash

// 配置異步 NSURLConnection 的方式

// 實例化一個 NSURLConnection,並將自身(SDWebImageDownloaderOperation)設置爲 NSURLConnection 實例的委託
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
// 由於上一步的 startImmediately:NO,因此這裏手動的觸發 start
// 這樣的效果和直接 startImmediately:YES 是同樣的
[self.connection start];
// 由於上面兩步結合起來或者直接 startImmediately:YES 的結果就是下載例程將會在當前 Run Loop 上以默認的模式進行調度,
// 而在 iOS 中除了主線程以外的線程都是默認沒有運行 Run Loop 的,因此須要手動的運行一下
CFRunLoopRun();
// 以後的代碼將會被 CFRunLoopRun() 所阻塞,這樣 operation 所在的線程
// 就不會自動的退出,因而須要額外的代碼在下載完成以後手動的中止 RunLoop 使得
// operation 所在的線程能夠退出
複製代碼

對於下載進度的監聽,SDWebImageDownloaderOperation 是經過將自身設置爲 NSURLConnection 委託的形式完成的:多線程

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 在這一委託方法的實現中,SDWebImageDownloaderOperation 主要是獲取服務端響應的 meta 信息,嘗試根據響應的 statusCode 對下載過程進行預判,好比若是是 304 狀態碼直接從本地緩存中返回圖片。可是這裏的代碼寫的有些繁瑣了,而且性能上也是存在些問題。首先能夠看下這幅概覽圖:

URL Loading System

上面就是 URL Loading System 的層次結構,可見 NSHTTPURLResponseNSURLResponse 惟一的子類,而且含有其父類沒有的 statusCode 方法。因而使用 isKindOfClass: 來判斷參數是不是 NSHTTPURLResponse 就能夠了,使用 respondsToSelector: 沒有額外的好處並且丟失了性能,見 Performance penalty using respondsToSelector

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 經過實現這個委託方法,就能夠知道有 new response chunk 被接收,因而能夠向外提供 progress,另外 SD 還實現了 display image progressively,按照代碼中的描述,出自於這裏 Progressive image download with ImageIO,其中有一小段是說 iOS 的實現相對於 Mac 須要點額外的步驟,而我將其示例代碼下載了以後,在註釋掉其中關於 iOS 適配的部分代碼後運行,發現註釋掉也是能夠的:

/// Create the image
        CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
        if (image) {
//#ifdef __IPHONE_4_0 // iOS
// CGImageRef imgTmp = [self createTransitoryImage:image];
// if (imgTmp) {
// [_delegate downloadedImageUpdated:imgTmp];
// CGImageRelease(imgTmp);
// }
//#else // Mac OS
// [_delegate downloadedImageUpdated:image];
//#endif
            [_delegate downloadedImageUpdated:image];
            CGImageRelease(image);
        }
複製代碼

也就是說這段 L290 代碼實際是有一點性能問題的,應該找到一個臨界的版本號以此適配老版本,而不是直接 TARGET_OS_IPHONE

還有一點在使用時須要注意的就是,若是須要得到具體的 progress 百分比,那麼在 new chunk 到達的時候,除了須要知道已經下載了的 chunks 的 size 總和以外,還須要知道 Content-Length,也就是在這裏試圖經過響應的 meta 信息(HTTP Headers)中獲取 expectedContentLength

而根據 HTTP 協議的描述 [1, 2],若是服務端的響應採用了 chunked 的方式,那麼客戶端實現必須忽略服務端響應中的 Content-Length(若是有的話。按照標準定義,在使用 chunked 時,服務端也應該不返回 Content-Length,固然通常狀況下也無法返回),換句話說,若是服務端響應的圖片信息使用 chunked transfer encoding 的話,那麼客戶端在圖片沒有徹底下載好以前就沒法知道圖片的總大小,因而試圖顯示一個下載百分比的進度條就不行了。這段算是 tips 吧。

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection,須要知道下載完成的時間點,故實現了這個委託方法

在另外的一些委託方法中,SD 完成了取消下載的相應操做,以及當請求的 HTTPS 證書不可信時的操做,以及當服務端資源須要訪問受權時的操做。

小結

SD 經過 SDWebImageDownloaderOperation 將圖片的下載操做封裝成 NSOperation,在內部經過設置 NSURLConnection 爲異步的方式,並將自身設置爲 NSURLConnection 委託,從而向外部提供下載進度控制的功能。

圖片緩存

下一步須要分析的就是 SD 的緩存機制,首先從 SD 的 README 中得知 SD 提供了常見了 two-levels cache 機制,即 memory-disk 的方式。在上一段分析下載的過程裏,發現 SD 下載圖片仍是藉由的 NSURLConnection,從 Understanding Cache Access 得知,iOS 中的 URL loading system 已經自帶了 two-levels cache 的機制,那麼爲何 SD 須要本身再實現一套呢?SD 本身是這樣解釋的,完整的解釋見 How is SDWebImage better than X?,大概的意思就是:

雖然 NSURLCache 提供了 two-levels cache,可是它緩存的內容是 raw bytes,也就是說從 NSURLCache 中取出的是圖片的 raw bytes,若是須要使用圖片還須要進行進一步的操做,好比解析圖片的信息,使其成爲在 iOS 中可使用的形式。而 SD 的緩存的則是將解析後的能夠在 iOS 中直接使用的圖片,這樣從緩存中取回的內容就不須要在解析一遍了,從而進一步節約了系統資源。

進一步瞭解 two-levels cache 或者 N-levels cache,其核心思想就是將須要緩存的內容放到多個 cache storages 中,而後在取出緩存內容時,儘可能的從響應速度較快的 storage 中取回。那麼很明顯,對於 memory-disk 這樣的 two-levels cache,無非就是將須要緩存的內容同時放到 memory 和 disk 中,而後取回的時候先嚐試較快的 storage,那麼勢必先檢索 memory cache storage,若是 memory cache 沒有命中的話,則嘗試 disk cache storage。下一步就是分析 SD 中具體是如何完成這些工做的。

首先 SD 中使用 SDWebImageManager 去集中管理圖片的下載操做,而且 SDWebImageManager 使用了單例的模式,在其初始化操做是這樣的:

- (id)init {
    if ((self = [super init])) {
    	 // 初始化 two-levels cache,它以 SDImageCache 的單例去操做
        _imageCache = [self createCache];
        // 以單例的形式初始化 SDWebImageDownloader
        _imageDownloader = [SDWebImageDownloader sharedDownloader];
        // 存放失敗的 URLs,爲了 re-try 的判斷
        _failedURLs = [NSMutableSet new];
        // 正在運行的 operations,方便統一的管理
        _runningOperations = [NSMutableArray new];
    }
    return self;
}
複製代碼

執行下載操做的是 SDWebImageManager 中的這個方法(具體的實如今 L110):

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
複製代碼

downloadImageWithURL 的具體實現中,使用了 SDWebImageCombinedOperation 來統一管理兩個操做(主要是取消的功能),一個操做就是先嚐試從緩存中取回圖片,另外一個操做就是若是緩存沒有命中,嘗試從源地址下載的操做。這樣只要取消 SDWebImageCombinedOperation 就會同時取消那兩個操做。

在下載的 subOperation 中,使用了 weakOperationL183

這是由於 這裏,若是在 subOperation 中沒有使用 weakOperation 的話,那麼就會發生 retain cycle

retain                            retain
+---------------------------------+           +---------------------+           +----------------------+
|   SDWebImageCombinedOperation   +----------->     cancelBlock     +----------->     subOperation     |
+----------------^----------------+           +---------------------+           +-----------+----------+
                 |                                                                          |
                 |                                                                          |
                 |                                                                          |
                 |                                    retain                                |
                 +--------------------------------------------------------------------------+

複製代碼

另外因爲須要在 self.runningOperationadd/remove SDWebImageCombinedOperation 的實例,因此加上了 __block 修飾

因爲 SDWebImageManager 是單例的形式,而其可能在多線程的狀況下被調用,因此對於其非線程安全的屬性,在操做時使用了 @synchronized 來確保數據的完整性。

具體的業務邏輯是這樣的:

  1. 首先從 SD 本身的緩存存儲中嘗試取回圖片 L149
  2. 若是在 SD 本身的緩存存儲中沒有取到圖片,或者選項中標記須要刷新緩存,那麼此時就須要從源地址下載圖片,可是以前還須要判斷下源地址是否容許被下載 L158
  3. L159 的意思是,若是選項標記須要刷新緩存,可是在本地緩存中找到了相關圖片,那麼就先使用這個緩存的圖片調用下 completedBlock,而後再繼續進行下載操做。

其實這一步放得有些散了,它是和 L180 以及 L216 搭配起來的。經過 L180,當發現 Response 是被 NSURLCache 緩存的,那麼 L216 的條件就會知足,爲何會知足呢?由於 這裏,因而 downloadedImagenil

知足條件了因而就什麼也沒作(要作的在 L159 已經被作了)。也就是說一旦設置了 SDWebImageRefreshCached 選項,那麼在使用 NSURLConnection 下載的時候,發現 Response 是此前緩存的,那麼就直接從 SD 的緩存中返回處理好的圖片,這麼作的緣由上文已經說過了 NSURLCache 的緩存是數據的 raw bytes,而 SD 中緩存的圖片數據是 out of the box。 4. 若是新下載了圖片,那麼確定是要先將其存儲在 SD 緩存中,SD 提供了緩存選項可讓調用者決定是單存 memory 或 disk 或 both,見 L237

上面主要是分析了 SDWebImageManager 在下載圖片時的操做,即先檢索本地 SD 緩存,而後再根據下載選項決定是否從源地址進行下載,以及下載好圖片以後將其存放到 SD 緩存中。

併發下載

在第一節中介紹了 SD 將下載操做封裝爲了 SDWebImageDownloaderOperation。SD 內部在使用時,並非直接操做 SDWebImageDownloaderOperation 的,而是使用的 SDWebImageDownloader 單例,在 SDWebImageDownloader 單例初始化的時候,產生了一個 NSOperationQueue,見 L67,而且設置了對了的併發數爲 6,見 L68。而後在須要下載的時候,將 SDWebImageDownloaderOperation 實例添加到了其內部的下載隊列中,這要就完成了併發下載的功能。

緩存的細節

如今開始分析下 SD 中的一些關於緩存操做的細節。檢索本地 SD 緩存分爲兩步,當檢索 memory cache storage 時,採用的是同步的方式,這是由於內存緩存的操做速度是很快的,當檢索 disk cache storage 時,SD 使用的是異步的方式,見 L372。SD 將緩存存儲以及其相關的操做封裝爲 SDImageCache 而且以單例的模式進行操做,SDImageCache 的初始化在 SDWebImageManager 的初始化中進行調用。

有一點須要注意的就是,SD 中實現的 sharedXXX 方法並不能表示一個確切的單例模式,具體的描述見 Create singleton using GCD's dispatch_once in Objective C,若是用其餘面嚮對象語言描述的話就是,必須將構造函數隱藏起來不要讓外部調用到,好比設置成 private,而後提供一個相似 getSingleton 的靜態方法。不過就像上面的連接中描述的同樣,若是口頭約定老是使用 sharedXXX 方法來獲取實例對象的話那也沒有太大的問題。

對於異步的檢索磁盤的方式,SD 採用的是 GCD,首先在 SDImageCache 初始化時建立了一個 ioQueue,注意 SD 中採用的是一個 serial queue,見 L99。使用 serial queue 的目的就是免得使用鎖去管理磁盤數據的讀寫了。

對於內存緩存,SD 實現了一個內部類 AutoPurgeCache,它繼承自 NSCache,功能就是在經過 Notification 來接受內存不足的通知,而後清除自身存儲緩存所佔用的內存空間。可是注意到一個細節,好比在 L106,看到下面的代碼:

dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
複製代碼

爲何須要在主線程上 postNotificationName:(注:如遇到方法的簽名我沒有寫全的狀況請沒必要在乎) 呢?

具體的內容在 Notification Programming Topics,大概的意思就是:

Regular notification centers deliver notifications on the thread in which the notification was posted. Distributed notification centers deliver notifications on the main thread. At times, you may require notifications to be delivered on a particular thread that is determined by you instead of the notification center. For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

上面的一段引用其實說了幾點內容,不過當前只須要知道第一句的意思:一般狀況下 notification center 會把 posted notifications 派送給與 post 動做所在的同一線程中的 observers。而上面的 L106 中的代碼能夠看出,它指望的 observers 是在主線程的,那麼 observers 就能夠在主線程中更新 UI 來給用戶相關的進度提示。

那爲何須要 dispatch_async 呢?這是由於 Notification Centers 中描述的:

A notification center delivers notifications to observers synchronously. In other words, when posting a notification, control does not return to the poster until all observers have received and processed the notification. To send notifications asynchronously use a notification queue, which is described in Notification Queues

再看 AutoPurgeCache 中註冊的 observer L24,observer 註冊在 AutoPurgeCache 運行時所在的線程,根據上面的第一段引用中的描述,對於 local notification 而言,postor 和 receiver 須要在同一線程,因而就猜想是否是對於系統通知而言,會在全部的線程上進行 notify。可是沒有在 Apple Doc 中找到明確的相關文字描述,不過進過測試確實對於系統通知而言,notifition center 會對進程中的全部線程進行 notify。下面是測試的代碼:

@interface Worker : NSThread
@end

@implementation Worker

- (void)main
{
    NSLog(@"Worker is running...");
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"Worker is exiting...");
}

- (void)testNotification
{
    NSLog(@"testNotification");
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    Worker* worker = [[Worker alloc] init];
    [worker start];
}
複製代碼

能夠運行模擬器而後 Hardware -> Simulate Memory Warning 就能夠看到子線程是能夠接收到通知的。

以上就是我閱讀源碼後的分析,雖然沒有面面俱到,也仍是但願能有所幫助。

[2015-11-24 修正]

上面有一段這樣說到:

另外因爲須要在 self.runningOperation  中 add/remove  SDWebImageCombinedOperation  的實例,因此加上了 __block  修飾

我今天回頭看了一下,發現我以前那樣的描述是不對的。

首先能夠看下這裏的描述,大概意思就是說若是須要讓那些被 block 所 captured 變量是 mutable 的,那麼就須要使用 __block 前綴去修飾。

那麼看看上面提到的 SD 中的代碼,簡化後就是這樣:

// 這裏的 __block 不須要
 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];

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

	@synchronized (self.runningOperations) {
	    [self.runningOperations removeObject:operation];
	}
}];

return operation;
複製代碼

注意到在 cacheOperation 那一行產生的 block,它對 operation 進行了 capture,可是在 block 內部並無改變 operation 的指向。因此這裏的 __block 是不須要的。Obj 對象在 block 是以引用去操做的,能夠想象是對象的內存地址被捕獲,若是是這樣就須要加上 __block

__block SDWebImageCombinedOperation *operation = nil;

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    // 捕獲這 operation,然而咱們須要改變它的內容
    // 把它的內容變成新對象的地址
    // 因此上面使用了 __block 前綴修飾
    operation = [SDWebImageCombinedOperation new]
  
	@synchronized (self.runningOperations) {
		[self.runningOperations addObject:operation];
	}

	@synchronized (self.runningOperations) {
	    [self.runningOperations removeObject:operation];
	}
}];

return operation;
複製代碼

我看可使用下面的代碼來驗證下上面的說法:

//
// main.m
// __block
//
// Created by mconintet on 11/24/15.
// Copyright © 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        static NSMutableArray* arr;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            arr = [[NSMutableArray alloc] init];
        });

        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

        NSInteger opCount = 3;

        for (NSInteger i = opCount; i > 0; i--) {
            NSOperation* op = [[NSOperation alloc] init];

            dispatch_async(queue, ^{
                [arr addObject:op];
            });

            dispatch_async(queue, ^{
                [arr removeObject:op];
                if (![arr count]) {
                    dispatch_semaphore_signal(sema);
                }
            });
        }

        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"arr count: %ld", [arr count]);
    }
    return 0;
}
複製代碼

對比下這段代碼:

//
// main.m
// __block
//
// Created by mconintet on 11/24/15.
// Copyright © 2015 mconintet. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        static NSMutableArray* arr;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            arr = [[NSMutableArray alloc] init];
        });

        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

        NSInteger opCount = 3;

        for (NSInteger i = opCount; i > 0; i--) {
            NSOperation* op;

            dispatch_async(queue, ^{
                op = [[NSOperation alloc] init]
                [arr addObject:op];
            });

            dispatch_async(queue, ^{
                [arr removeObject:op];
                if (![arr count]) {
                    dispatch_semaphore_signal(sema);
                }
            });
        }

        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"arr count: %ld", [arr count]);
    }
    return 0;
}
複製代碼

你會發現後一段代碼會被 IDE 提示:

爲何不能賦值?由於指針的捕獲也是做爲了 const,和基本類型同樣。

總結起來講就是,objc 對象在 block 中捕獲的是指向其真實地址的指針,指針以 const 的形式被捕獲,不使用 __block 修飾就沒法改變指針的內容,可是對於指針指向的對象,它們的內容仍是能夠改變的。

上面的關於 NSNotification 的說明有些紕漏,修正見 NSNotificationCenter

相關文章
相關標籤/搜索