通讀SDWebImage①--整體梳理、下載和緩存

要寫點關於SDWebImage的文章了,這段時間看的很多,整體的感覺是SDWebImage的代碼不如AFN那麼規整、有條理,並無吐槽的意思,稍微細細看一下就會有這樣的感覺。本篇文章不會用大量的篇幅來介紹SDWebImage如何使用,而是更多地介紹SDWebImage的總體思路和一些實現細節,還有介紹一些不是特別經常使用的一些功能(由於有很多iOS開發人員還只是會使用sd_setImageWithURL)。首先咱們要看一下SDWebImage的總體結構:
SDWebImage總體結構圖
這裏我要說明的一點是我當前使用SD的git提交版本是e41af47e2f5de9317d55083e23168e076b550e34(Sat Jan 30 02:54:23 2016 +0100)。讓咱們看一下這張圖的內容。
能夠將SDWebImage的框架分爲三個部分:git

1.適配
SDWebImage
iOS版本、編譯指令的適配、線程切換的宏、還有一個導出的內聯函數,用於根據image的命名key 將image轉換成相應的scale的UIImage類型以完成對Image的縮放,如文件名帶@2x,將按照2倍縮放。github

2.Util工具
核心的類就是SDWebImageManager它負責建立和管理下載任務、對緩存操做進行管理,咱們一般使用的UIImageView的WebCache分類下的sd_setImageWithURL方法的實現就依賴於這個類,其餘View分類的設置圖片的方法也實現也相似。
SDWebImageManager實現下載依賴於下載器:SDWebImageDownloader,下載器負責管理下載任務,而執行下載任務是由SDWebImageDownloaderOperation操做完成。
SDWebImageManager實現緩存依賴於緩存管理:SDImageCache,可以完成圖片的內存緩存和磁盤緩存,還能夠查詢指定url的圖片是否進行了緩存、取出緩存等操做。
下載和緩存的過程當中會調用適配模塊進行將圖片轉爲合適的尺寸,使用解壓模塊將被壓縮的圖片解壓後完成緩存。web

3.分類
包括兩部分:①.視圖分類、②.用於圖片格式處理和格式轉換的模塊。數組

①.視圖分類
視圖分類中有一個基本的分類:
UIView+WebCacheOperation這個分類用於完成將組合操做(SD定義了可以實現下載和緩存的組合操做類SDWebImageCombinedOperation)與View綁定、取消綁定和移除綁定等功能。其餘視圖分類的實現都依賴於這個分類。
MKAnnotationView+WebCache、UIImageView+WebCache、UIImageView+HighlightedWebCache對view中的圖片的加載過程的實現比較類似(後面會介紹),UIButton+WebCache分類中針對UIButton的不一樣的State能夠設置不一樣的image。瀏覽器

②.用於圖片格式處理和格式轉換的模塊
NSData+ImageContentType這個分類只有一個方法sd_contentTypeForImageData:,是根據圖片的二進制data的第一個字節的數據,獲得圖片相應的MIME類型。
UIImage+MultiFormat也只有一個方法sd_imageWithData:,根據傳入的NSData,讀取到MIME類型而後轉換成對應的UIImage。
UIImage+GIF根據傳入的值如文件名或者NSData,獲得對應的GIF圖的UIImage對象,其實是一個animatedImage。
UIImage+WebP根據傳入的NSData,獲得對應的WebP圖的UIImage對象,這個方法的實現依賴於WebP庫,須要到google下載libwebp。緩存

以上是從代碼的角度分析了SD能夠完成的工做,而在github上SD的主頁能夠看到,它的自我介紹中的主打功能:服務器

提供UIImageView的一個分類,以支持網絡圖片的加載與緩存管理
一個異步的圖片加載器
一個異步的內存+磁盤圖片緩存
支持GIF圖片
支持WebP圖片
後臺圖片解壓縮處理
確保同一個URL的圖片不被下載屢次
確保虛假的URL不會被反覆加載
確保下載及緩存時,主線程不被阻塞

本篇文章的內容主要涉及到4個類:SDWebImageDownloaderOptionsSDWebImageDownloaderSDImageCacheSDWebImageManager,詳細介紹如何實現下載和緩存的以及如何在這個過程當中作到上面提到的‘三個確保’。至於其餘內容(如GIF和WebP圖片的加載)之後會一一介紹。
cookie

下載操做SDWebImageDownloaderOptions和下載過程實現

SDWebImage下載圖片使用的是SDWebImageDownloaderOperation,它是一個NSOperation的子類,同時遵照了<SDWebImageOperation>協議(其實這個協議只聲明瞭一個方法cancel用於取消操做)。這個操做負責管理下載的選項,進行網絡訪問時的request,設置網絡處理質詢的憑據,進行網絡鏈接接收數據,管理網絡訪問的response和是否解壓的選項等。總之,它的任務就是網絡訪問配置、進行網絡訪問以及處理數據。網絡

每個NSOperation都是爲了完成一項任務而誕生的,而SDWebImageDownloaderOperation的任務就是負責依照指定的下載選項,使用將指定的urlRequest建立NSURLConnection對象進行網絡鏈接(NSURLConnection對象的代理就是SDWebImageDownloaderOperation本身),進行對圖片的下載。在下載的過程當中對圖片數據進行拼接,能夠實現對進度progress的跟蹤,在下載以後能夠將接收到的圖片數據轉換、解壓等,並完成一個下載完成的回調。若是網路訪問過程當中接收到質詢,則使用服務端憑據或者本地存儲的憑據處理質詢;若是下載失敗了,則發送錯誤通知,執行完成回調,並結束下載任務。app

SDWebImageDownloaderOperation類主要有如下幾個屬性:
1.NSURLRequest *request:下載時進行網絡請求的request,由構造方法傳入。
2.BOOL shouldDecompressImages:下載後是否須要解壓圖片。
3.BOOL shouldUseCredentialStorage:URLConnection是否須要諮詢憑據倉庫來對鏈接進行受權,默認是YES。
這是NSURLConnectionDelegate的-connectionShouldUseCredentialStorage:方法的返回值
4.NSURLCredential *credential:在-connection:didReceiveAuthenticationChallenge:方法中驗證質詢時使用的憑據
已經存在的request,URL的用戶名或密碼構成的憑據會覆蓋這個值,具體解釋參見SDWebImageDownloader部分。
5.SDWebImageDownloaderOptions options:readonly下載選項,由構造方法傳入。
6.NSInteger expectedSize:預期的文件長度,使用NSInteger徹底夠用。
7.NSURLResponse *response:connection對象進行網絡訪問,接收到的的response
要注意的是:下載選項是在SDWebImageDownloader中定義的,SDWebImageDownloader是下載器負責管理下載隊列和控制下載過程(經過調用SDWebImageDownloaderOperation的方法)。下載選項SDWebImageDownloaderOptions的定義以下:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    /// 漸進式下載,若是設置了這個選項,會在下載過程當中,每次接收到一段chunk數據就調用一次完成回調(注意是完成回調)回調中的image參數爲未下載完成的部分圖像
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /// 一般狀況下request阻止使用NSURLCache. 這個選項會用默認策略使用NSURLCache 
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /// 若是從NSURLCache中讀取圖片,會在調用完成block時,傳遞空的image或imageData \
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    /// 系統爲iOS 4+時,若是應用進入後臺,繼續下載。這個選項是爲了實如今後臺申請額外的時間來完成請求。若是後臺任務到期,操做會被取消。
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /// 經過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來處理存儲在NSHTTPCookieStore的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /// 容許不受信任的SSL證書,在測試環境中頗有用,在生產環境中要謹慎使用
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /// 將圖片下載放到高優先級隊列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

這些選項主要涉及到下載的優先級、緩存、後臺任務執行、cookie處理以及證書認證幾個方面,在建立下載操做的時候可使用組合的選項以完成一些特殊的需求。

SDWebImageDownloaderOperation只對外提供了一個對象方法- initWithRequest: options: progress: completed: cancelled:,它使用默認的屬性值初始化一個SDWebImageDownloaderOperation對象。

下面咱們看一下SDWebImageDownloaderOperation對NSOperation的-start方法的重寫,畢竟這是完成下載任務的核心代碼。如下是將-start提取出來的部分代碼

@synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset]; // 將各個屬性置空。包括取消回調、完成回調、進度回調,用於網絡鏈接的connection,用於拼接數據的imageData、記錄當前線程的屬性thread。
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
 // 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系統借用一點時間,繼續執行下面的代碼來完成connection的建立和進行下載任務。
 // 在後臺任務執行時間超過最大時間時,也就是後臺任務過時執行過時回調。在回調主動將這個後臺任務結束。
 /*
        ^{
                __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]; // 建立用於下載的connection
        self.thread = [NSThread currentThread]; // 記錄當前線程
    }

    [self.connection start]; 

    if (self.connection) {
        if (self.progressBlock) { // 任務開始馬上執行一次進度回調
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        dispatch_async(dispatch_get_main_queue(), ^{ // 發送開始下載的通知,object爲operation自己
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
        
        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }
        // 當runloop開啓以後,線程切換到runloop中的任務,開始下載圖片,因此下面的代碼是通過一段時間的延遲執行的,也就是當connection的網絡訪問進行以後,纔會執行下面的代碼。
        // 這個時候能夠進行一些判斷,如圖片是否被正確地下載完成。
        if (!self.isFinished) {
            [self.connection cancel];

            // NSURLConnectionDelegate代理方法
            // 主動調用 並製造一個錯誤,這樣作的目的是由於這個方法一旦調用,代理就不會再接收connection的消息,也就是不在調用其餘的任何代理方法了,connection完全結束。
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    } else { // connectin 建立失敗,這裏直接執行完成回調,並傳遞一個connection沒有初始化的錯誤
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

    // 運行到這裏說明下載操做已經完成(不管成功仍是失敗),所以沒有必要在後臺運行。使用endBackgroundTask:
#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

這些就是一次下載操做要執行的任務,可是數據處理是下載任務的關鍵,SDWebImageDownloaderOperation經過NSURLConnection的代理方法完成對下載的圖片的數據處理,主要用到如下幾個方法:

// NSURLConnectionDataDelegate中聲明
connection: didReceiveResponse: // 接收到服務端的response時,執行一次
connection: didReceiveData: // 每次接收到chunk數據都會調用
connectionDidFinishLoading: // 當鏈接結束的時候調用一次
connection: willCacheResponse: // 要進行緩存以前調用

// NSURLConnectionDelegate中聲明 
connection: didFailWithError: // 鏈接失敗,或者沒有成功下載完成調用
connectionShouldUseCredentialStorage: // 指定是否須要使用本地憑據進行驗證
connection: willSendRequestForAuthenticationChallenge: // 處理服務端過來的質詢

下面咱們看一下除了上面標註的一些基本的功能外,SDWebImageDownloaderOperation在每一個方法內部還有哪些細節性的工做。
1.connection: didReceiveResponse:
主要完成如下工做:

if (statusCode<400而且不等於304) {
    // 設置預期文件長度屬性的值
    // 馬上完成一次進度回調,傳遞的參數爲0,
    // 初始化用於拼接圖片二進制數據的屬性imageData
    // 設置response屬性爲服務端返回的response值
    // 向主隊列同步發送一個接收到response的通知
} else {
    // 若是statusCode爲304,也就是服務端Not Modified而且拿到了本地的HTTP緩存,取消操做,發送操做中止的通知,執行完成回調,中止當前的runloop,設置下載完成標記爲YES,正在執行標記爲NO,將屬性置空。
}

2.connection: didReceiveData:

使用自身屬性imageData拼接接收的數據
if (下載選項設置了SDWebImageDownloaderProgressiveDownload) {
    取得已經拼接完的imageData,建立一個CGImageSourceRef類型的imageSouce,使用imageSouce建立CGImageRef類型的對象partialImageRef,表明着要下載的圖片的一部分,調整方向並將使用`UIImage imageWithCGImage:partialImageRef`將其導出爲UIImage,釋放掉partialImageRef,並在主線程同步執行一次完成回調,指定第一個參數爲剛纔處處的UIImage,最後釋放掉imageSource佔用的空間。
}
執行一次進度回調progressBlock,第一個參數傳遞已經拼接的imageData的長度

3.connectionDidFinishLoading:
執行完這個方法以後,代理不會再接收任何connection發送的消息,意味着下載完成。一般狀況下,下載任務正常結束以後,就會執行一次這個方法。

@synchronized(self) {
    中止當前的RunLoop,將connection屬性和thread屬性置空,發送下載中止的通知。
}
檢查sharedURLCache是否緩存了此次下載response,若是沒有就將responseFromCached設置爲NO
執行完成回調completionBlock,並根據是否讀取了緩存、圖片尺寸是否爲(0,0)等條件向完成回調傳遞不一樣的值。
將完成狀態、執行狀態的標記復位、將屬性置空

4.connection: didFailWithError:
執行完這個方法以後,代理不會再接收任何connection發送的消息,意味着下載失敗。一般狀況下,下載任務非正常結束,就會執行一次這個方法。

@synchronized(self) {
        中止當前的RunLoop,將connection屬性和thread屬性置空,發送下載中止的通知。
}

if (self.completedBlock) { // 只使用這一種參數傳遞的方式完成回調
    self.completedBlock(nil, nil, error, YES);
}
將完成狀態、執行狀態的標記復位、將屬性置空

5.connection: willCacheResponse:
緩存response以前調用一次這個方法,給connection的代理一次機會改變它。能夠返回一個修改以後的response,或者返回nil不存儲緩存。
SDWebImageDownloaderOperation在這個方法內部完成了如下工做:

responseFromCached = NO; // 標記此次下載的圖片不是從緩存中讀取出來的
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { 
    return nil;  // 若是request的緩存策略(實際上Downloader在使用操做進行下載的時候,會根據下載選項修改request的緩存策略)是忽略本地緩存,不進行不進行緩存
} else {
    return cachedResponse; // 其餘狀況,正常進行緩存
}

6.connectionShouldUseCredentialStorage:
這個代理方法的返回值決定URL加載器是否須要使用存儲的憑據對網絡進行受權驗證。
SDWebImageDownloaderOperation中這樣實現:

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
    return self.shouldUseCredentialStorage; // shouldUseCredentialStorage屬性的init方法中賦初值YES,提供了對外的setter,能夠在外部修改這個值。
}

7.connection: willSendRequestForAuthenticationChallenge:
服務端發起一個質詢,須要在這個方法中解決。SDWebImageDownloaderOperation對這個方法的實現比較複雜:

if (服務端要求的認證方式是信任認證) {
    若是下載選項沒有設置容許無效的SSL證書這個下載選項,那麼按照默認的方式處理質詢
    其餘狀況,就直接使用服務端發過來的憑據繼續訪問
} else {
    若是這個質詢以前沒有受權失敗過且self.credential存在(也就是想操做賦值了一個本地的憑據),使用self.credential做爲憑據處理質詢

    其餘狀況直接使用沒有憑據的方式處理質詢。
}

以上就是全部的SDWebImageDownloaderOperation內部實現的NSURLConnection的代理方法,這些方法已經可以很好地完成網絡訪問、圖片下載和數據處理。
SDWebImageDownloaderOperation中還定義了一些取消操做的方法,用於暫停下載任務,這些方法比較簡單,這裏再也不一一贅述。
在上面的數據處理所有的過程當中,咱們發現時刻都在使用者下載選項,可見熟悉每一個下載選項和使用時機的重要性。接下來看一下負責管理下載操做的SDWebImageDownloader類,同時下載選項枚舉也是在這個類的頭文件中聲明的。

下載管理SDWebImageDownloader

若是說SDWebImageDownloaderOperation實現了下載圖片的細節,那麼SDWebImageDownloader就負責控制operation來觸發下載任務,並管理全部的下載任務,包括改變他們的狀態,SDWebImageDownloader是進行下載控制的接口,在實際應用中,咱們幾乎不多直接使用SDWebImageDownloaderOperation,而幾乎都是使用SDWebImageDownloader來進行下載任務。
SDWebImageDownloader有一個重要的屬性executionOrder表明着下載操做執行的順序,它是一個SDWebImageDownloaderExecutionOrder枚舉類型:

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    // 默認值,全部的下載操做以隊列類型 (先進先出)執行.
    SDWebImageDownloaderFIFOExecutionOrder,

    // 全部的下載操做以棧類型 (後進先出)執行.
    SDWebImageDownloaderLIFOExecutionOrder
};

默認是SDWebImageDownloaderFIFOExecutionOrder,是在init方法中設置的。若是設置了後進先出,在下載操做添加到下載隊列中時,會依據這個值添加依賴關係,使得最後添加操做出在依賴關係鏈條中的第一項,於是會優先下載最後添加的操做任務。
SDWebImageDownloader還提供了其餘幾個重要的對外接口(包括屬性和方法):
1.BOOL shouldDecompressImages
是否須要解壓,在init中設置默認值爲YES,在下載操做建立以後將值傳遞給操做的同名屬性。
解壓下載或緩存的圖片能夠提高性能,可是會消耗不少內存
默認是YES,若是你會遇到由於太高的內存消耗引發的崩潰將它設置爲NO。
2.NSInteger maxConcurrentDownloads
放到下載隊列中的下載操做的總數,是一個瞬間值,由於下載操做一旦執行完成,就會從隊列中移除。
3.NSUInteger currentDownloadCount
下載操做的超時時長默認是15.0,即request的超時時長,若設置爲0,在建立request的時候依然使用15.0。
只讀。
4.NSURLCredential *urlCredential
爲request操做設置默認的URL憑據,具體實施爲:在將操做添加到隊列以前,將操做的credential屬性值設置爲urlCredential
5.NSString *usernameNSString *passwords
若是設置了用戶名和密碼:在將操做添加到隊列以前,會將操做的credential屬性值設置爲[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession],而忽略了屬性值urlCredential。
6.- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
爲HTTP header設置value,用來追加到每一個下載對應的HTTP request, 若傳遞的value爲nil,則將對應的field移除。
擴展裏面定義了一個HTTPHeaders屬性(NSMutableDictionary類型)用來存儲全部設置好的header和對應value。
在建立request以後緊接着會將HTTPHeaders賦給request,request.allHTTPHeaderFields = self.HTTPHeaders;
7.- (NSString *)valueForHTTPHeaderField:(NSString *)field;
返回指定的HTTP header field對應的value
8.SDWebImageDownloaderHeadersFilterBlock headersFilter
設置一個過濾器,爲下載圖片的HTTP request選取header.意味着最終使用的headers是通過這個block過濾以後的返回值。
9.- (void)setOperationClass:(Class)operationClass;
設置一個SDWebImageDownloaderOperation的子類 ,在每次 SDWebImage 構建一個下載圖片的請求操做的時候做爲默認的NSOperation使用.
參數operationClass爲要設置的默認下載操做的SDWebImageDownloaderOperation的子類。 傳遞 nil 會恢復爲 SDWebImageDownloaderOperation

如下兩個方法是下載控制方法了

- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
這個方法用指定的URL建立一個異步下載實例。
有關completedBlock回調的一些解釋:下載完成的時候block會調用一次.
沒有使用SDWebImageDownloaderProgressiveDownload選項的狀況下,若是下載成功會設置image參數,若是出錯,會根據錯誤設置error參數. 最後一個參數老是YES. 若是使用了SDWebImageDownloaderProgressiveDownload選項,這個block會使用部分image的對象有間隔地重複調用,同時finished參數設置爲NO,直到使用完整的image對象和值爲YES的finished參數進行最後一次調用.若是出錯,finished參數老是YES.

- (void)setSuspended:(BOOL)suspended;
設置下載隊列的掛起(暫停)狀態。若爲YES,隊列再也不開啓新的下載操做,再向隊列裏面添加的操做也不會被開啓,可是正在執行的操做依然繼續執行。

下面咱們就來看一下下載方法的實現細節:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self) wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        // 建立下載的回調
    }];

    return operation;
}

重點就是addProgressCallback: completedBlock: forURL: createCallback:的執行了,SDWebImageDownloader將外部傳來的進度回調、完成回調、url直接傳遞給這個方法,並實現建立下載操做的代碼塊做爲這個方法的createCallback參數值。下面就看一下這個方法的實現細節:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // 對URL判空,若是爲空,直接執行完成回調。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    /*
    對dispatch_barrier_sync函數的解釋:
     向分配隊列提交一個同步執行的barrier block。與dispatch_barrier_async不一樣,這個函數直到barrier block執行完畢纔會返回,在當前隊列調用這個函數會致使死鎖。當barrier block被放進一個私有的並行隊列後,它不會被馬上執行。實際爲,隊列會等待直到當前正在執行的blocks執行完畢。到那個時刻,隊列纔會本身執行barrier block。而任何放到 barrier block以後的block直到 barrier block執行完畢纔會執行。
     傳遞的隊列參數應該是你本身用dispatch_queue_create函數建立的一個並行隊列。若是你傳遞一個串行隊列或者全局並行隊列,這個函數的行爲和 dispatch_sync相同。
     與dispatch_barrier_async不一樣,它不會對目標隊列進行強引用(retain操做)。由於調用這個方法是同步的,它「借用」了調用者的引用。並且,沒有對block進行Block_copy操做。
     做爲對其優化,這個函數會在可能的狀況下在當前線程喚起barrier block。
     */
    
    // 爲確保不會死鎖,當前隊列是另外一個隊列,而不能是self.barrierQueue。
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        /*
        URLCallbacks字典類型key爲NSURL類型,value爲NSMutableArray類型,value只包含着一個元素,這個元素是一個NSMutableDictionary類型,它的key爲NSString表明着回調類型,value爲block,是對應的回調
        */
        // 同一時刻對相同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第一次綁定它的回調,也就是第一次使用這個url建立下載任務,則執行一次建立回調。
            在建立回調中建立下載操做,dispatch_barrier_sync執行確保同一時間只有一個線程操做URLCallbacks屬性,也就是確保了下面建立過程當中在給operation傳遞迴調的時候能取到正確的self.URLCallbacks[url]值。同時保證後面有相同的url再次建立時,if (!self.URLCallbacks[url])分支再也不進入,first==NO,也就再也不繼續調用建立回調。這樣就確保了同一個url對應的圖片不會被重複下載。

            而下載器的完成回調中會將url從self.URLCallbacks中remove,雖然remove掉了,可是再次使用這個url進行下載圖片的時候,Manager會向緩存中讀取下載成功的圖片了,而不是無腦地直接添加下載任務;即便以前的下載是失敗的(也就是說沒有緩存),這樣繼續添加下載任務也是合情合理的。
            // 所以準確地說,將這個block放到並行隊列dispatch_barrier_sync執行確保了,同一個url的圖片不會同一時刻進行屢次下載.
            
            // 這樣作還使得下載操做的建立同步進行,由於一個新的下載操做尚未建立完成,self.barrierQueue會繼續等待它完成,而後才能執行下一個添加下載任務的block。因此說SD添加下載任務是同步的,並且都是在self.barrierQueue這個並行隊列中,同步添加任務。這樣也保證了根據executionOrder設置依賴關是正確的。換句話說若是建立下載任務不是使用dispatch_barrier_sync完成的,而是使用異步方法 ,雖然依次添加建立下載操做A、B、C的任務,但實際建立順序可能爲A、C、B,這樣當executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設置的操做依賴關係就變成了A依賴C,C依賴B
            // 可是添加以後的下載依然是在下載隊列downloadQueue中異步執行,絲絕不會影響到下載效率。

            // 以上就是說了SD下載的關鍵點:建立下載任務在barrierQueue隊列中,執行下載在downloadQueue隊列中。
            */
        }
    });
}

說完這些,咱們再看一下SD如何給addProgressCallback: completedBlock: forURL: createCallback:方法設置建立回調的,畢竟這個纔是建立下載操做並放入隊列的一些細節:

[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
    NSTimeInterval timeoutInterval = wself.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    // 建立請求對象,並根據options參數設置其屬性
    // 爲了不潛在的重複緩存(NSURLCache + SDImageCache),若是沒有明確告知須要緩存,則禁用圖片請求的緩存操做, 這樣就只有SDImageCache進行了緩存
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    
    if (wself.headersFilter) {
        request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
    }
    else {
        request.allHTTPHeaderFields = wself.HTTPHeaders;
    }
    // 建立SDWebImageDownloaderOperation操做對象,傳入進度回調、完成回調、取消回調
    operation = [[wself.operationClass alloc] initWithRequest:request
                                                      options:options
                                                     progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                         // 從callbacksForURL中取出進度回調
                                                         SDWebImageDownloader *sself = wself;
                                                         if (!sself) return;
                                                         __block NSArray *callbacksForURL;
                                                         dispatch_sync(sself.barrierQueue, ^{
                                                             callbacksForURL = [sself.URLCallbacks[url] copy];
                                                         });
                                                         for (NSDictionary *callbacks in callbacksForURL) {
                                                             dispatch_async(dispatch_get_main_queue(), ^{ // 切換到主隊列完成異步回調
                                                                 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                 if (callback) callback(receivedSize, expectedSize);
                                                             });
                                                         }
                                                     }
                                                    completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                        // 從callbacksForURL中取出完成回調。
                                                        // 將刪除全部回調的block放到隊列barrierQueue中使用barrier_sync方式執行,確保了在進行調用完成回調以前全部的使用url對應的回調的地方都是正確的數據。
                                                        SDWebImageDownloader *sself = wself;
                                                        if (!sself) return;
                                                        __block NSArray *callbacksForURL;
                                                        dispatch_barrier_sync(sself.barrierQueue, ^{
                                                            callbacksForURL = [sself.URLCallbacks[url] copy];
                                                            if (finished) {
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            }
                                                        });
                                                        for (NSDictionary *callbacks in callbacksForURL) {
                                                            SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                            if (callback) callback(image, data, error, finished);
                                                        }
                                                    }
                                                    cancelled:^{
                                                        // 將url對應的全部回調移除
                                                        SDWebImageDownloader *sself = wself;
                                                        if (!sself) return;
                                                        dispatch_barrier_async(sself.barrierQueue, ^{
                                                            [sself.URLCallbacks removeObjectForKey:url];
                                                        });
                                                    }];
    // 設置是否須要解壓
    operation.shouldDecompressImages = wself.shouldDecompressImages;
    // 設置進行網絡訪問驗證的憑據
    if (wself.urlCredential) {
        operation.credential = wself.urlCredential;
    } else if (wself.username && wself.password) {
        operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
    }
    // 根據下載選項SDWebImageDownloaderHighPriority設置優先級
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    // 將操做添加到隊列中
    [wself.downloadQueue addOperation:operation];
    // 根據executionOrder設置操做的依賴關係
    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        [wself.lastAddedOperation addDependency:operation];
        wself.lastAddedOperation = operation;
    }
}];

有關NSOperation的優先級還有一個小細節:

queuePriority
這個屬性包含着操做的相對優先級。這個值會影響到操做出隊列和執行的順序,這個值老是符合一個系統預約義的常量。若是沒有明確設置優先級,則使用默認值NSOperationQueuePriorityNormal。
當且僅當須要對沒有依賴關係的操做之間設置優先級時候使用它。優先級值不該該用來實現對不一樣的操做對象之間的依賴管理。若是你想在操做之間創建依賴關係,應該使用addDependency:方法。

若是你嘗試指定一個和定義好的常量不一樣的優先級值,操做對象自動調整你指定的值以適應NSOperationQueuePriorityNormal優先級,直到找到有效的常量值。例如,若是你指定了這個值爲-10,操做會調整這個值來匹配NSOperationQueuePriorityVeryLow;類似的,若是你指定了這個值爲+10,操做會調整這個值來匹配NSOperationQueuePriorityVeryHigh常量。

另外,咱們能夠觀察到若是沒有給operationClass傳遞值的狀況下,SDWebImageDownloader的- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:方法實際上返回的是SDWebImageDownloaderOperation類型實例,而且是已經通過各類下載選項設置以後的放入到下載隊列中的操做實例。
咱們還須要關注一下方法- (void)setSuspended:(BOOL)suspended;,這個方法的實現只有一句:

- (void)setSuspended:(BOOL)suspended { 
    [self.downloadQueue setSuspended:suspended]; // 其實是對下載隊列調用了setSuspended方法
}

有關NSOperationQueue對象的setSuspended,不得不看一下文檔的一些解釋:

當這個屬性值是NO,隊列主動開啓在隊列中的操做,並準備執行。將這個屬性設置爲YES,阻止隊列開啓任何隊列式的操做,但已經開始且正在執行的操做會繼續執行。你能夠繼續向暫停的隊列添加操做,可是若是不改變這個屬性爲NO,這些操做不會計劃執行。
Operation當且僅當執行完成以後才從隊列中移除。可是,爲告終束執行,操做必須得先開啓執行。由於一個暫停的隊裏不能開啓任何一個新的操做,它不會移除任何一個在當前隊列中且不是正在執行的操做(包括已經取消的操做)。
你能夠經過KVO監控這個屬性值的改變。配置一個觀察者來監控操做隊列的suspended key path。
這個屬性的默認值是NO。

可見setSuspended方法傳遞YES,並不能暫停隊列中的全部操做,而是讓隊列再也不開啓新的任務。

以上就是關於SD下載圖片的所有內容。

緩存SDImageCache

SDImageCache類是一個功能無比強大的緩存管理器。它能夠實現內存和磁盤緩存功能的實現和管理,主要包括如下幾個方面:
1.對內存或磁盤緩存進行單個圖片增、刪、查等操做
2.還提供使用命名空間的方式對圖片分類管理,管理應用啓動前的放入app中的預緩存圖
3.同時還能夠對全部的緩存總體操做,如查詢總緩存文件個數,查詢總緩存大小,一次性清理內存緩存,一次性清理磁盤緩存。
並且剛纔所說的全部功能實現以後能夠添加完成回調,以便在主線程更新UI或者給出提示信息。

SDImageCache的主要屬性有如下幾個:
1.BOOL shouldDecompressImages
是否進行解壓
2.BOOL shouldDisableiCloud
不啓用iCloud備份 默認是YES
3.BOOL shouldCacheImagesInMemory
使用內存緩存 默認是YES
4.NSUInteger maxMemoryCost
內存緩存NSCache可以承受的最大總開銷,超過這個值NSCache會剔除對象。是內存緩存(NSCache類型)的屬性值。
5.NSUInteger maxMemoryCountLimit
內存緩存NSCache能承受的最多對象個數
6.NSInteger maxCacheAge
最大緩存時長 以秒爲單位, 默認值爲kDefaultCacheMaxCacheAge,一週時間
7.NSUInteger maxCacheSize
最大緩存大小 以字節爲單位。默認沒有設置,也就是爲0,而清理磁盤緩存的先決條件爲self.maxCacheSize > 0,因此0表示無限制。
在看看它的主要的方法,這裏將它們分爲幾個組分別說明:

有關命名空間,SD會根據命名空間,對內存緩存建立不一樣的NSCache對象並對name屬性賦值,建立不一樣的磁盤緩存寫文件隊列等等。

/**
 * 用指定的命名空間初始化一個新的緩存倉庫
 */
- (id)initWithNamespace:(NSString *)ns;

/**
 * 用指定的命名空間和目錄初始化一個新的緩存倉庫
 */
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;

- (NSString *)makeDiskCachePath:(NSString*)fullNamespace; // 獲取指定ns的完整路徑,這裏傳遞的是完整ns

/**
經過SDImageCache添加一個只讀的緩存文件夾路徑,用來搜索預緩存的圖片
 若是你想在你的app中捆綁預加載圖片,就很是有用。
 */
- (void)addReadOnlyCachePath:(NSString *)path;

什麼是完整的ns路徑,按照SD的規則(能夠在initWithNamespace: diskCacheDirectory:方法中查看),fullNameSpace是在ns前面添加了前綴com.hackemist.SDWebImageCache.內存緩存的memCache.name直接設置爲fullNamespace,若傳入的ns爲@"xyz"磁盤緩存的的路徑則變爲Library/Caches/xyz/com.hackemist.SDWebImageCache.xyz。

這裏還有兩個用於查詢指定key圖片在磁盤緩存中的路徑的方法

// 獲取指定key的緩存路徑,須要傳入root文件夾
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;

// 獲取指定key的默認文件路徑,也就是root文件夾使用self.diskCachePath
- (NSString *)defaultCachePathForKey:(NSString *)key;

如今介紹一些對單個圖片的緩存操做的方法:

增:

// 將指定的image緩存起來,key通常傳入urlString,默認進行磁盤緩存,實際實現爲下面方法toDisk傳入YES
- (void)storeImage:(UIImage *)image forKey:(NSString *)key; 

// toDisk指定爲是否進行磁盤緩存,實際實現爲調用了下面的方法
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;

/**
 * 將image作內存緩存,磁盤緩存爲可選
 
 * @param recalculate BOOL 表明着是否imageData可使用或者一個新的data會根據UIImage構建
 * @param imageData   imageData做爲服務器返回的數據, 這個值會用來作磁盤存儲來代替將給定的image轉換成可存儲的/壓縮的圖片格式的方案,以便節約性能和CPU。(其實是節約了計算能力,而多使用了一點磁盤的存儲能力)
 內存緩存都是使用image
 image和imageData都非空 若recalculate爲YES會忽略imageData,而使用image進行磁盤緩存
 二者有一個爲空的,使用非空的進行磁盤緩存
 二者都爲空,則沒有磁盤緩存。
 */
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

查:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock; // 異步查詢磁盤緩存,其實內部實現先查詢了內存緩存,而後查詢磁盤緩存,返回的是一個空的操做(稍後會解釋爲何是空的操做)


- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; // 異步查詢內存緩存

- (UIImage *)imageFromDiskCacheForKey:(NSString *)key; // 查詢完內存緩存以後再異步查詢磁盤緩存

刪:

// 異步地移除內存和磁盤緩存,實際是下面方法的withCompletion傳入nil
- (void)removeImageForKey:(NSString *)key;

// 異步地移除內存和磁盤緩存,帶完成回調
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion; 

// 異步地移除內存緩存,能夠選擇是否移除磁盤緩存,實際是下面方法的withCompletion傳入nil
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;

// 異步地移除內存緩存,能夠選擇是否移除磁盤緩存,完成以後執行回調
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

對於全部的緩存內容的總體操做,有以下一些方法:

刪:

// 清除全部的內存緩存
- (void)clearMemory;

// 清除全部的磁盤緩存,無阻塞的方法,馬上返回.
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;

// 上面的方法回調傳入nil
- (void)clearDisk;

// 移除磁盤中全部的過時緩存。無阻塞的方法
// clean和clear的區別是:clear是所有移除,clean只清除過時的緩存
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;

// 上面的方法回調傳入nil
- (void)cleanDisk;

查:

// 獲取磁盤緩存的大小
- (NSUInteger)getSize; // 是在當前線程的串行隊列中同步執行的,思路是遍歷目錄中的全部文件,累加大小

// 獲取磁盤緩存文件的個數
- (NSUInteger)getDiskCount;

// 異步計算磁盤緩存的大小,而後執行回調,回調參數爲文件個數和總大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

// 異步檢查圖片是否在磁盤緩存中存在(沒有加載圖片)
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;

// 上面方法回調參數傳遞nil
- (BOOL)diskImageExistsWithKey:(NSString *)key;

SDImageCache中的方法實現都比較簡單:

內存緩存
添加都是對memCache屬性添加元素,key爲urlString
刪除都是對memCache屬性移除元素
查詢都是按key取元素。

磁盤緩存
添加都是將UIImage的二進制寫入文件,並以url的MD5爲文件名(下面會具體分析)
刪除都是將緩存文件刪除
查詢都是讀取文件的二進制轉爲UIImage

對磁盤緩存總體的操做則是遍歷文件夾進行對單個文件操做來實現,在執行清理操做的時候,會一一對比緩存文件的上次修改(存儲)的時間到當前時間是否超過了過時時長,進行刪除操做(下面會具體分析)。而對於readonly的預先緩存好的圖片所在的路徑會存儲在私有屬性customPaths中,查詢圖片的時候也會遍歷這個屬性中全部的文件夾。

另外內存緩存的memCache是自定義的NSCache子類AutoPurgeCache,會接收內存警告的通知,當收到通知,會調用removeAllObjects方法清除全部的內存緩存。

對於實現一張圖片緩存的具體實現:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 內存緩存 前提是設置了須要進行
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    // 磁盤緩存
    if (toDisk) {
        // 將緩存操做做爲一個任務放入ioQueue中異步執行
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;

            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // 須要肯定圖片是PNG仍是JPEG。PNG圖片容易檢測,由於有一個惟一簽名。PNG圖像的前8個字節老是包含如下值:137 80 78 71 13 10 26 10
                // 在imageData爲nil的狀況下假定圖像爲PNG。咱們將其看成PNG以免丟失透明度。
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
                // 而當有圖片數據時,咱們檢測其前綴,肯定圖片的類型
                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
            }

            if (data) {
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                // 根據image的key獲取緩存路徑
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // 不適用iCloud備份
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

對於清理方法cleanDiskWithCompletionBlock:,有兩個指標:文件的緩存有效期及最大緩存空間大小。文件的緩存有效期能夠經過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];

        // 枚舉器預先獲取緩存文件的有用的屬性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 枚舉緩存文件夾中全部文件,該迭代有兩個目的:移除比過時日期更老的文件;存儲文件屬性以備後面執行基於緩存大小的清理操做
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 移除早於有效期的老文件
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // 存儲文件的引用並計算全部文件的總大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 若是磁盤緩存的大小超過咱們配置的最大大小,則執行基於文件大小的清理,咱們首先刪除最老的文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 以設置的最大緩存大小的一半值做爲清理目標
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // 按照最後修改時間來排序剩下的緩存文件
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // 刪除文件,直到緩存總大小降到咱們指望的大小
            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();
            });
        }
    });
}

咱們看一下剛纔遺留的一個問題,爲何使用- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock方法查詢指定key的緩存時,返回的是一個空的NSOperation,咱們先看一下這個方法的實現:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    // 對doneBlock、key判空 查找內存緩存
    // ...

    // 查找內存緩存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) { // isCancelled初始默認值爲NO
            return;
        }

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

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

經過代碼能夠看到operation雖然沒有具體的內容,可是咱們能夠在外部調用operation的cancel方法來改變isCancelled的值。這樣作對從內存緩存中查找到圖片的本次操做查詢過程沒有影響,可是若是本次查詢過程是在磁盤緩存中進行的,就會受到影響,autoreleasepool{}代碼塊再也不執行。而在這段代碼塊完成了這樣的工做:將磁盤緩存取出進行內存緩存,在線程執行完成回調。所以能夠看到這個返回的NSOpeation值能夠幫助咱們在外部控制再也不進行磁盤緩存查詢和內存緩存備份的操做,歸根結底就是向外部暴漏了取消操做的接口。

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;

@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// ...
@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類的核心所在。咱們來看看它的具體實現:

爲了可以更好地理解這個方法的實現,再次必須強調一個SDWebImageOptions選項值SDWebImageRefreshCached,若是設置了這個值:
即便SD對圖片緩存了,也指望HTTP響應cache control,並在須要的狀況下從遠程刷新圖片。也就是說若是在磁盤中找到了這張圖片,但設置了這個選項,仍然須要進行網絡請求,查看服務器端的這張圖片有沒有被改變,並決定進行下載,而後使用新的圖片,同時完成新的緩存。
可是這個下載並非本身決定要不要進行的,還須要若是代理經過方法[self.delegate imageManager:self shouldDownloadImageForURL:url]返回NO,那就是代理要求這個url對應的圖片不須要下載。這種狀況下就再也不下載,而是使用在緩存中查找到的圖片

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    // 判斷URL合法性 
    // ...

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 建立一個組合操做,主要用於將查詢緩存、下載操做、進行緩存等工做聯繫在一塊兒
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    
    // 檢查這個url是否在失敗列表中,也就是是否曾經下載失敗過。
    // ...

    // 若是沒有設置失敗重試選項(SDWebImageRetryFailed),而且是一個失敗過的url,則直接執行完成回調。
    // ...

    @synchronized (self.runningOperations) { // (self.runningOperations是一個數組,元素爲正在進行的組合操做)
        [self.runningOperations addObject:operation];
    }
    NSString *key = [self cacheKeyForURL:url];

    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        // 若是操做被取消了,從正在進行的操做列表中將它移出.
        // ...
        
        // 條件A:在緩存中沒有找到圖片 或者 options選項包含SDWebImageRefreshCached (這兩種狀況都須要進行請求網絡圖片的)
        // 且
        // 條件B:代理容許下載
        /*
         條件B的實現爲:代理不能響應imageManager:shouldDownloadImageForURL:方法 或者 能響應且方法返回值爲YES。也就是說沒有實現這個方法就是容許的,而若是實現了的話,返回爲YES纔是容許的。
         */
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            // 分支一:緩存中找到了圖片 且 options選項包含SDWebImageRefreshCached, 先在主線程完成一次回調,使用的是緩存中找到的圖片
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // 若是在緩存中找到了image可是設置了SDWebImageRefreshCached選項,傳遞緩存的image,同時嘗試從新下載它來讓NSURLCache有機會接收服務器端的更新
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }
            
            // 若是沒在緩存中找到image 或者 設置了須要請求服務器刷新的選項,則仍須要下載.
            SDWebImageDownloaderOptions downloaderOptions = 0;
            // ...
            if (image && options & SDWebImageRefreshCached) {
                // 若是image已經被緩存可是設置了須要請求服務器刷新的選項,強制關閉漸進式選項
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // 若是image已經被緩存可是設置了須要請求服務器刷新的選項,忽略從NSURLCache讀取的image
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            // 建立下載操做,先使用self.imageDownloader下載
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { // 使用self.imageDownloader進行下載
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 若是操做取消了,不作任何事情
                    //若是調用completedBlock, 這個block會和另外一個completedBlock爭奪同一個對象。所以,若是這個block後被調用,會覆蓋新的數據。
                }
                else if (error) {
                    // 進行完成回調
                    // 將url添加到失敗列表中
                    // ...
                }
                else {
                    // 若是設置了失敗重試,將url從失敗列表中去掉
                    // ...
                    
                    // 設置了SDWebImageRefreshCached選項 且 緩存中找到了image 且 沒有下載成功
                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // 這個分支的進入的條件:既沒有error、downloadedImage又是nil,這種回調在SDWebImageDownloaderOperation進行下載的時候只有讀取了URL的緩存纔會發生,即下載正常完成,可是沒有數據。
                        // 圖片刷新遇到了NSSURLCache中有緩存的情況,不調用完成回調。
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    // 下載成功 且 設置了須要變形Image的選項 且變形的代理方法已經實現
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        /*
                         全局隊列異步執行:
                         1.調用代理方法完成形變
                         2.進行緩存
                         3.主線程執行完成回調
                         */
                         // ...
                    }
                    else {
                        /*
                         1.進行緩存
                         2.主線程執行完成回調
                         */
                        // ...
                    }
                }

                if (finished) {
                    // 從正在進行的操做列表中移除這個組合操做
                    // ...
                }
            }];
            // 設置組合操做的取消回調
            // ...
        }
        // 處理其餘狀況
        // 狀況一:在緩存中找到圖片(代理不容許下載 或者 沒有設置SDWebImageRefreshCached選項 知足至少一項)
        else if (image) {
            // 使用image執行完成回調
            // 從正在進行的操做列表中移除組合操做
            // ...
        }
        // 狀況二:在緩存中沒找到圖片 且 代理不容許下載
        else {
            // 執行完成回調
            // 從正在進行的操做列表中移除組合操做
            // ...
        }
    }];

    return operation;
}

這個方法主要完成了這些工做:
1.建立一個組合Operation,是一個SDWebImageCombinedOperation對象,這個對象負責對下載operation建立和管理,同時有緩存功能,是對下載和緩存兩個過程的組合。
2.先去尋找這張圖片 內存緩存和磁盤緩存,這兩個功能在self.imageCache的queryDiskCacheForKey: done:方法中完成,這個方法的返回值既是一個緩存operation,最終被賦給上面的Operation的cacheOperation屬性。
在查找緩存的完成回調中的代碼是重點:它會根據是否設置了SDWebImageRefreshCached選項和代理是否支持下載決定是否要進行下載,並對下載過程當中遇到NSURLCache的狀況作處理,還有下載失敗的處理以及下載以後進行緩存,而後查看是否設置了形變選項並調用代理的形變方法進行對圖片形變處理。
3.將上面的下載方法返回的操做命名爲subOperation,並在組合操做operation的cancelBlock代碼塊中添加對subOperation的cancel方法的調用。這樣就完成了下面的工做1和2:

// 1.使能經過組合操做的屬性cacheOperation控制緩存操做的取消
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    // ...
    // 須要下載的話,進行下面的過程
    id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {

    }
    operation.cancelBlock = ^{ // 2.使能經過組合操做的cancelBlock控制下載的取消
        [subOperation cancel];
         // ...
    };

    // 不須要下載的其餘狀況
    // ...
}

4.處理請他的狀況:代理不容許下載可是找到緩存的狀況,沒有找到緩存且代理不容許下載的狀況
5.這個方法最終返回的是operation也就是一個SDWebImageCombinedOperation對象,而不是下載操做。
注意如下區分:

本方法,也就是SDWebImageManager對象的- (id <SDWebImageOperation>)downloadImageWithURL:options:progress:completed:返回的是SDWebImageCombinedOperation對象

SDImageCache對象的- (NSOperation *)queryDiskCacheForKey: done:返回的是一個空的NSOperation對象(用於取消磁盤緩存查詢和內存緩存備份)

SDWebImageDownloader對象的- (id <SDWebImageOperation>)downloadImageWithURL:options: progress:completed:返回的是一個已經放到隊列中執行的下載操做,默認是SDWebImageDownloaderOperation對象

介於幾個方法的語義不明 我強烈建議SD作一下修改:

將SDWebImageOperation協議更名爲SDCancellableOperation


將SDWebImageManager對象的- (id <SDWebImageOperation>)downloadImageWithURL:options:progress:completed:方法更名爲- (id <SDWebImageCombinedOperation>)downloadAndCacheImageWithURL:options:progress:completed:
介於沒有對外暴漏SDWebImageCombinedOperation類,更名爲- (id <SDCancellableOperation>)downloadAndCacheImageWithURL:options:progress:completed:便可


將SDImageCache對象的- (NSOperation *)queryDiskCacheForKey: done:更名爲- (id <SDCancellableOperation>)queryDiskCacheForKey: done:,固然這個不是必須的


將SDWebImageDownloader對象的- (id <SDWebImageOperation>)downloadImageWithURL:options: progress:completed:更名爲
- (id <SDCancellableOperation>)downloadImageWithURL:options: progress:completed:

說了那麼半天尚未介紹一項重要內容:上面這個下載方法中的操做選項參數是由枚舉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,
};

能夠看到兩個SDWebImageOptions與SDWebImageDownloaderOptions中的選項有必定的對應關係,實際上咱們在使用SD時,使用SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法較多,而幾乎不多單獨使用下載和緩存的功能,這個方法的組合功能中會使用設置的SDWebImageOptions值改變相應的SDWebImageDownloaderOptions值,同時也會對緩存方案有必定的一項。

SDWebImageManager中還有一個重要的屬性:
決定緩存的key的使用方案的屬性@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
這是一個block類型的值,會按照它定義的內容對url進行過濾,獲得url對應的緩存key。還有一個根據url獲得緩存key的方法,其內部就是調用了這個block。

- (NSString *)cacheKeyForURL:(NSURL *)url; // 若是外部沒有傳入self.cacheFilter 那麼返回的是[url absoluteString]

SDWebImageManager中還有一些控制和查看執行狀態的方法:

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

// 檢查是否有操做在運行,這裏的操做指的是下載和緩存組成的組合操做,其實就是檢查self.runningOperations中的組合操做個數是否大於0
- (BOOL)isRunning;

另外要說明SDWebImageManager中還定義了與緩存操做相關的方法,其實都是調用了self.imageCache(SDImageCache類型)的相關緩存方法實現的,如:

// 使用self.imageCache的store..方法進行內存和磁盤緩存
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;

// 指定url的圖片是否進行了緩存,優先查看內存緩存,再查看磁盤緩存,只要有就返回YES,二者都沒有則返回NO
- (BOOL)cachedImageExistsForURL:(NSURL *)url;

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


- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock; // 獲取指定url的緩存傳遞給回調,若是是內存緩存,在主隊列異步執行回調;若是是磁盤緩存,在當前線程執行回調

- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock; // 獲取指定url的磁盤緩存,只是磁盤緩存,和上面的實現相同,在當前線程執行回調

這些方法的具體實現能夠查看本文第三部分:緩存SDImageCache

相關文章
相關標籤/搜索