SDWebImage(v3.7.6) 源碼學習

2018.9.24html

1、 使用

1. 使用 UIImageView+WebCache

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

在 block 中獲得圖片下載進度和圖片加載完成(下載完成或者讀取緩存)的回調,若是你在圖片加載完成前取消了請求操做,就不會收到成功或失敗的回調ios

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
	                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]
	                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
	                                ... completion code here ...
	                             }];
複製代碼

2. 單獨使用 Manager/Downloader/Cache

單獨使用 SDWebImageManagergit

SDWebImageManager *manager = [SDWebImageManager sharedManager];
	[manager loadImageWithURL:imageURL
	                  options:0
	                 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
	                        // progression tracking code
	                 }
	                 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
	                    if (image) {
	                        // do something with image
	                    }
	                 }];
複製代碼

單獨使用 SDWebImageDownloader 異步下載圖片github

SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
	[downloader downloadImageWithURL:imageURL
	                         options:0
	                        progress:^(NSInteger receivedSize, NSInteger expectedSize) {
	                            // progression tracking code
	                        }
	                       completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
	                            if (image && finished) {
	                                // do something with image
	                            }
	                        }];
複製代碼

單獨使用 SDImageCache 異步緩存圖片 SDImageCache 支持內存緩存和異步的磁盤緩存(可選),可使用單例,也能夠建立一個有獨立命名空間的 SDImageCache 實例。 添加緩存的方法:segmentfault

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
複製代碼

默認狀況下,圖片數據會同時緩存到內存和磁盤中,若是你想只要內存緩存的話,可使用下面的方法:數組

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
複製代碼

讀取緩存時可使用 queryDiskCacheForKey:done: 方法,圖片緩存的 key 是惟一的,一般就是圖片的 absolute URL。緩存

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
	[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
	    // image is not nil if image was found
	}];
複製代碼

2、結構

1. 模塊

模塊圖

  • 下載(SDWebImageDownloader)
  • 緩存(SDImageCache)
  • 將緩存和下載的功能組合起來(SDWebImageManager)
  • 封裝成 UIImageView/UIButton 的分類方法(UIImageView+WebCache 等)

MKAnnotationView :地圖大頭針 屬於 MapKit 框架的一個類,繼承自 UIView,是用來展現地圖上的 annotation 信息的,它有一個用來設置圖片的屬性 image。 官方文檔 MKMapView 的使用安全

2. 目錄結構

目錄結構

3. 核心邏輯

流程圖

流程圖圖源:J_Knight_:SDWebImage源碼解析,講解清晰,十分感謝。bash

在一個UIImageView調用: sd_setImageWithURL: placeholderImage: options: progress: completed:markdown

  • 取消當前正在進行的加載任務 operation
  • 設置佔位圖
  • 若是 URL 不爲 nil,就經過 Manager 單例開啓圖片加載的operation

downloadImageWithURL:options:progress:completed: 中會先拿圖片緩存的 key (默認是圖片 URL)去 Cache 單例中讀取內存緩存,若是有,就返回給 Manager; 若是沒有,就開啓異步線程,拿通過 MD5 處理的 key 去讀取磁盤緩存,若是找到磁盤緩存了,就同步到內存緩存,而後再返回給 ManagerdownloadImageWithURL會返回一個 SDWebImageCombinedOperation 對象,這個對象包含一個 cacheOperation 和一個 cancelBlock。

若是內存和磁盤緩存中都沒有圖片,Manager 就會調用 Downloader 單例的 -downloadImageWithURL: options: progress: completed: 方法去下載,先將傳入的 progressBlockcompletedBlock 保存起來,並在第一次下載該 URL 的圖片時,建立一個 NSMutableURLRequest 對象和一個 SDWebImageDownloaderOperation 對象,並將該 SDWebImageDownloaderOperation 對象添加到downloadQueue 來啓動異步下載任務。

DownloaderOperation 中包裝了一個 NSURLConnection 的網絡請求,並經過 runloop 來保持 NSURLConnection 在 start 後、收到響應前不被幹掉,下載圖片時,監聽 NSURLConnection 回調的 -connection:didReceiveData: 方法中會負責 progress 相關的處理和回調,- connectionDidFinishLoading: 方法中會負責將 data 轉爲 image,以及圖片解碼操做,並最終回調 completedBlock。

DownloaderOperation 中的圖片下載請求完成後,會回調給 Downloader,而後 Downloader 再回調給 ManagerManager將圖片緩存到內存和磁盤上(可選),並回調給 UIImageViewUIImageView 中再回到主線程設置 image 屬性。

4. 調用時序圖

時序圖

3、實現策略

1. Cache 緩存策略

SDImageCache 管理着一個內存緩存和磁盤緩存(可選),同時在寫入磁盤緩存時採起異步執行,不會阻塞主線程。

爲何須要緩存?

  • 以空間換時間,速度更快
  • 減小沒必要要的網絡請求,節省流量

1.1 內存緩存

內存緩存經過一個繼承 NSCacheAutoPurgeCache 類實現。

NSCache (NSCache官方文檔) NSCache 是一個相似於 NSMutableDictionary 存儲 key-value 的容器,特色以下:

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

1.2 磁盤緩存

磁盤緩存經過異步操做 NSFileManager 存儲緩存文件到沙盒實現。

1.3 緩存操做

  1. 初始化 -init 方法中默認調用了 -initWithNamespace: 方法,-initWithNamespace: 方法又調用了 -makeDiskCachePath: 方法來初始化緩存目錄路徑, 同時還調用了 -initWithNamespace:diskCacheDirectory: 方法來實現初始化。 初始化方法調用棧:
-init
    -initWithNamespace:
        -makeDiskCachePath: 
        -initWithNamespace:diskCacheDirectory:
複製代碼

-initWithNamespace:diskCacheDirectory: 初始化實例變量、屬性,設置屬性默認值,並根據 namespace 設置完整的緩存目錄路徑,除此以外還添加了通知觀察者,用於內存緊張時清空內存緩存,以及程序終止運行時和程序退到後臺時清掃磁盤緩存。

  1. 寫入緩存 storeImage:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk{
}
複製代碼

寫入的參數有三個。 添加內存緩存時,先計算像素,再加進去 添加磁盤緩存時,若是須要在存儲以前將傳進來的 image 轉成 NSData,而不是直接使用傳入的 imageData,那麼就要按不一樣的圖片格式來轉成對應的 NSData 對象。

NSData 用來包裝數據,存儲的是二進制數據,屏蔽了數據之間的差別,文本、音頻、圖像等數據均可用NSData來存儲。

判斷圖片格式:根據是否有 alpha 通道以及 imageData 的前8位字節 判斷圖片格式詳解 若是 imageData 爲 nil,就根據 image 是否有 alpha 通道來判斷圖片是不是 PNG 格式的 若是 imageData 不爲 nil,就根據 imageData 的前 8 位字節來判斷是否是 PNG 格式,由於 PNG 圖片有一個惟一簽名,前 8 位字節是(十進制): 137 80 78 71 13 10 26 10

拿到 imageData 後,藉助 NSFileManager 將圖片二進制存儲到沙盒,存儲的文件名是對 key 進行 MD5 處理後生成的字符串。 默認沙盒路徑: Library - Caches

iOS的沙盒機制 SandBox 一種安全體制,規定應用程序只能在爲該應用建立的文件夾內讀取文件,不能訪問其餘地方的內容。保存全部的非代碼文件,如圖片,聲音,屬性列表和文本文件等。 應用程序向外請求或接收數據都須要通過權限認證。 默認狀況下,每一個沙盒含有3個文件夾:Documents, Library 和 tmp

  • Documents:保存應用運行時生成的須要持久化的數據,iTunes會備份該目錄。
  • Library:存儲程序的默認設置或其它狀態信息;
    • Caches:保存應用運行時生成的須要持久化的數據,通常存儲體積大、不須要備份的非重要數據。iTunes不會備份此目錄,此目錄文件不會在應用退出時刪除。
    • Preferences:偏好設置文件,iTunes會備份該目錄。
  • tmp:保存應用運行時所需的臨時數據,使用完畢後再將相應的文件從該目錄刪除。應用沒有運行時,系統也可能會清除該目錄下的文件。iTunes不會備份該目錄。
  1. 讀取緩存 queryDiskCacheForKey
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
}
複製代碼

返回的是一個 NSOperation 對象 這個方法會先讀取內存緩存,若是沒有再讀取磁盤緩存。 讀取磁盤緩存時,會先從沙盒中去找,若是沙盒中沒有,再從 customPaths (也就是 bundle)中去找。 找到以後,對數據進行轉換,後面的圖片處理步驟跟圖片下載成功後的圖片處理步驟同樣——先將 data 轉成 image,再進行根據文件名中的 @2x、@3x 進行縮放處理,若是須要解壓縮,最後再解壓縮一下。

  1. 清掃磁盤緩存 每新加載一張圖片,就會新增一份緩存,因此須要按期清除部分緩存。
  • 清掃磁盤緩存 (clean):刪除部分緩存文件
  • 清空磁盤緩存 (clear):刪除整個緩存目錄

指標

  • 緩存有效期:經過 maxCacheAge 屬性設置,默認一週
  • 緩存體積最大限制:經過 maxCacheSize 來設置的,默認爲 0。

SDImageCache 在初始化時添加了通知觀察者,因此在應用即將終止時和退到後臺時,都會調用 -cleanDiskWithCompletionBlock: 方法來異步清掃緩存。 清掃磁盤緩存(clean): 遍歷全部緩存文件,若是設置了 maxCacheAge(最大緩存不過時時間) 屬性的話,先刪掉過時的文件,同時記錄文件的屬性和整體積大小,把文件按修改時間從早到晚排序,再遍歷這個文件數組,一個一個刪,直到整體積小於 desiredCacheSize 爲止,也就是 maxCacheSize 的一半。

2. Downloader 下載策略

主要任務

  • 異步下載圖片管理
  • 圖片加載優化

具體實現: +initialize 中主要是經過註冊通知 讓SDNetworkActivityIndicator 監聽下載事件,來顯示和隱藏狀態欄上的 network activity indicator。 爲了讓 SDNetworkActivityIndicator 文件能夠不用導入項目中來(若是不要的話),這裏使用了 runtime 的方式來實現動態建立類以及調用方法。

+ (void)initialize {
	if (NSClassFromString(@"SDNetworkActivityIndicator")) {
		id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
		# 先移除通知觀察者 SDNetworkActivityIndicator
		# 再添加通知觀察者 SDNetworkActivityIndicator
	}
}
複製代碼

+sharedDownloader 方法中調用了 -init 方法來建立一個單例,-init方法中作了一些初始化設置和默認值設置,包括設置最大併發數(6)、下載超時時長(15s)等。

核心方法: - downloadImageWithURL: options: progress: completed: 方法 首先經過調用 -addProgressCallback: andCompletedBlock: forURL: createCallback: 方法來保存每一個 url 對應的回調 block -addProgressCallback: ... 方法先進行錯誤檢查,判斷 URL 是否爲空,而後再將 URL 對應的 progressBlockcompletedBlock 保存到 URLCallbacks 屬性中。

URLCallbacks 屬性是一個 NSMutableDictionary 對象,key 是圖片的 URL,value 是一個數組,包含每一個圖片的多組回調信息。

由於可能同時下載多張圖片,因此就可能出現多個線程同時訪問 URLCallbacks 屬性的狀況。爲了保證線程安全,因此這裏使用了 dispatch_barrier_sync 來分步執行添加到 barrierQueue 中的任務,這樣就能保證同一時間只有一個線程能對 URLCallbacks 進行操做。

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
	//1. 判斷 url 是否爲 nil,若是爲 nil 則直接回調 completedBlock,返回失敗的結果,而後 return,由於 url 會做爲存儲 callbacks 的 key
	//2. 處理同一個 URL 的屢次下載請求(MARK: 使用 dispatch_barrier_sync 函數來保證同一時間只有一個線程能對 URLCallbacks 進行操做):
	//3. 從屬性 URLCallbacks(一個字典) 中取出對應 url 的 callBacksForURL(這是一個數組,由於可能一個 url 不止在一個地方下載)
	//4. 若是沒有取到,也就意味着這個 url 是第一次下載,那就初始化一個 callBacksForURL 放到屬性 URLCallbacks 中
	//5. 往數組 callBacksForURL 中添加 包裝有 callbacks(progressBlock 和 completedBlock)的字典
	//6. 更新 URLCallbacks 存儲的對應 url 的 callBacksForUR 
}
複製代碼

若是這個 URL 是第一次被下載,就要回調 createCallbackcreateCallback 主要作的就是建立並開啓下載任務

createCallback 方法中調用了 - [SDWebImageDownloaderOperation initWithRequest: options: progress:] 方法來建立下載任務 SDWebImageDownloaderOperation

3. 其餘

3.1 SDWebImageDecoder

因爲 UIImage 的 imageWithData 函數是每次畫圖的時候纔將 Data 解壓成 ARGB 的圖像,因此在每次畫圖的時候都會有一個解壓操做,這樣雖然只有瞬時的內存需求,可是效率很低。 爲了提升效率,經過 SDWebImageDecoder 將包裝在 Data 下的資源畫在另一張圖片上,這樣這張新圖片就再也不須要重複解壓了,是空間換時間的作法。

圖片的解碼實際是將圖片的二進制數據轉換成像素數據的過程,SD 對圖片進行從新繪製,獲得一張位圖。 顯示圖片須要 RGBA 的色彩空間(什麼是 RGBA ?),可是 PNG 和 JPEG 自身的格式非 RGBA。因此建立一個 BitmapImage,先在非UI線程渲染圖片,做爲預解碼,而後拿到UIImage去顯示。 iOS 圖片解碼

3.2 SDWebImagePrefetcher

能夠預先下載,可是下載是低優先級的。

4、TIPS

用 NSOperation 進行操做管理

1. NSOperation 的特性

  • 狀態 State operation 的執行過程: isReady -> isExecuting -> isFinished

經過 keypath 的 KVO 通知來隱式的獲得 state ,而不是顯式的經過一個 state 的屬性。當一個 operation 已經準備就緒,將要被執行時,它會爲 isReadykeyPath 發送一個KVO的通知,對應的屬性值也會變爲YES.

爲了構造一致的狀態,每一個屬性都與其餘屬性相互排斥: isReady : 若是 operation 已經作好了執行的準備返回YES,若是它所依賴的操做存在一些未完成的初始化步驟則返回NO。 isExecuting :若是 operation 正在執行它的任務返回YES,不然返回NO。 isFinished : 任務成功的完成了執行,或者中途被 Cancel ,返回YES。

NSOperationQueue 只會把 isFinished 爲 YES 的 operation 踢出隊列, isFinished 爲 NO 的永遠不會被移除,因此實現時要保證其正確性,避免死鎖發生

  • 取消 Cancellation 取消一個 operation 的兩種狀況:
    • 顯式的調用cancel方法
    • operation 依賴的其餘 operation 執行失敗

NSOperation 的被取消也是經過 isCancelledkeypath 的 KVO 來得到。當 NSOperation 的子類覆寫 cancel 方法時,注意清理掉內部分配的資源。 特別注意的是,這時 isCancelled 和 isFinished 的值都變爲了 YES, isExecuting 爲值變爲NO。

cancel : 帶一個」l」 表示方法 (動詞) isCancelled : 帶兩個」l」表示屬性(形容詞)

  • 優先級 Priority 設置 queuePriority 屬性就能夠提高和下降 operation 的優先級, queuePriority 屬性可選的值以下: NSOperationQueuePriorityVeryHigh NSOperationQueuePriorityHigh NSOperationQueuePriorityNormal NSOperationQueuePriorityLow NSOperationQueuePriorityVeryLow 另外,operation 能夠指定一個 threadPriority 值,它的取值範圍是0.0到1.0,1.0表明最高的優先級。 queuePriority:決定執行順序的優先級 threadPriority:決定 operation 開始執行以後分配的計算資源的多少

  • 依賴 Dependencies 若是須要把一個大的任務分紅多個子任務,可使用依賴,來保證前後執行順序。 B 操做若是依賴於 A,則必須在 A operation 的 isFinished 爲 YES 的時候纔會開始執行。 【避免循環依賴產生死鎖】

[resizingOperation addDependency:networkingOperation];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];
複製代碼
  • completionBlock 當一個NSOperation完成以後,就會精確地只執行一次completionBlock。 Eg.當一個網絡請求結束以後,能夠在 completionBlock 裏處理返回的數據。

參考:Mattt - NSOperation

2. Manager 中如何使用 NSOperation

SDWebImageCombinedOperation 當 url 被正確傳入以後, 會實例一個很是奇怪的 「operation」, 它實際上是一個遵循 SDWebImageOperation 協議的 NSObject 的子類. 而這個協議也很是的簡單:

@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
複製代碼

這裏僅僅是將這個 SDWebImageOperation 類包裝成一個看着像 NSOperation 其實並非 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;
    }
}
複製代碼

而這個類, 應該是爲了實現更簡潔的 cancel 操做而設計出來的.

3. Downloader 中如何使用 NSOperation

每張圖片的下載都會發出一個異步的 HTTP 請求,由 DownloaderOperation 管理。

DownloaderOperation 繼承 NSOperation,遵照 SDWebImageOperationNSURLConnectionDataDelegate 協議。

SDWebImageOperation 協議只定義了一個方法 -cancel,用來取消 operation。

當建立的 DownloaderOperation 對象被加入到 downloaderdownloadQueue 中時,該對象的 -start 方法就會被自動調用。 -start 方法中首先建立了用來下載圖片數據的 NSURLConnection,而後開啓 connection,同時發出開始圖片下載的 當圖片的全部數據下載完成後,Downloader 傳入的 completionBlock 被調用,圖片下載結束。

所以圖片的數據下載是由一個 NSConnection 對象來完成的,這個對象的整個生命週期(從建立到下載結束)是由 DownloaderOperation 來控制的,將 operation 加入到 operation queue 中就能夠實現多張圖片同時下載了

其餘小 TIPS

NS_OPTIONS 枚舉類型的使用 使用 NS_OPTIONS 位運算枚舉類型,可同時 經過「與」運算符,能夠判斷是否設置了某個枚舉選項,由於每一個枚舉選擇項中只有一位是1,其他位都是 0,因此只有參與運算的另外一個二進制值在一樣的位置上也爲 1,與 運算的結果纔不會爲 0. Eg. 0101 (至關於 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache) & 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache) = 0100 (> 0,也就意味着 option 參數中設置了 SDWebImageDownloaderUseNSURLCache)

初始化 通常來講,一個管理類都有一個全局的單例對象,根據業務需求設計不一樣的初始化方法。在設計類的時候,應該經過合理的初始化方法告訴別的開發者,該類應該如何建立。

  • (nonnull instancetype)sharedImageCache 單例
  • (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns 經過制定的namespace來初始化
  • (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER 指定namespace和path.

使用@synchronized: 在 Manager 對 failedURLsrunningOperations作操做時均使用了@synchronized,在新版本里換成了 GCD 實現

下載高分辨率圖,致使內存暴增的解決辦法

5、反思

1. 與最新版本(v4.4.2)

功能擴展

  • 使用 FLAnimatedImage 來處理動圖
  • 增長了 SDImageCacheConfig 對緩存進行配置,能夠選擇是否解壓緩存、iCloud、最大緩存大小。
  • 大圖縮放邏輯:sd_decompressedAndScaledDownImageWithImage: 避免要縮放的圖片太大,採用的方式是將圖片分割成一系列大小的小方塊,而後每一個方塊去獲取 Image 並 draw 到目標 BitmapContext 上

2. 緩存優化

對於緩存如何作優化?

  • 增刪查找的速度
  • 提升命中率

(1). -> LRU -> LRU+FIFO

(2). 緩存模糊匹配 針對同一圖片,不一樣大小的請求。若是緩存中有更大的圖片,也視爲命中緩存。

相關文章
相關標籤/搜索