SDWebImage(v3.7.6) 原始碼學習

Hanniya發表於2019-01-07

2018.9.24 HanniyaZhang

[一個個人學習記錄,原始碼版本較落後,參考意義不大,今年會更新對 SD 最新版本的原始碼閱讀]

一、 使用

1. 使用 UIImageView+WebCache

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

在 block 中得到圖片下載進度和圖片載入完成(下載完成或者讀取快取)的回撥,如果你在圖片載入完成前取消了請求操作,就不會收到成功或失敗的回撥

	[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

單獨使用 SDWebImageManager

	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 非同步下載圖片

	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 例項。 新增快取的方法:

	[[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
	}];
複製程式碼

二、結構

1. 模組

模組圖

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

MKAnnotationView :地圖大頭針 屬於 MapKit 框架的一個類,繼承自 UIView,是用來展示地圖上的 annotation 資訊的,它有一個用來設定圖片的屬性 image。 官方文件 MKMapView 的使用

2. 目錄結構

目錄結構

3. 核心邏輯

流程圖

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

在一個UIImageView呼叫: sd_setImageWithURL: placeholderImage: options: progress: completed:

  • 取消當前正在進行的載入任務 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. 呼叫時序圖

時序圖

三、實現策略

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

5. 其他

5.1 SDWebImageDecoder

由於 UIImage 的 imageWithData 函式是每次畫圖的時候才將 Data 解壓成 ARGB 的影象,所以在每次畫圖的時候都會有一個解壓操作,這樣雖然只有瞬時的記憶體需求,但是效率很低。 為了提高效率,通過 SDWebImageDecoder 將包裝在 Data 下的資源畫在另外一張圖片上,這樣這張新圖片就不再需要重複解壓了,是空間換時間的做法。

圖片的解碼實際是將圖片的二進位制資料轉換成畫素資料的過程,SD 對圖片進行重新繪製,得到一張點陣圖。 顯示圖片需要 RGBA 的色彩空間(什麼是 RGBA ?),但是 PNG 和 JPEG 自身的格式非 RGBA。所以建立一個 BitmapImage,先在非UI執行緒渲染圖片,作為預解碼,然後拿到UIImage去顯示。 iOS 圖片解碼

5.2 SDWebImagePrefetcher

可以預先下載,但是下載是低優先順序的。

四、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 實現

下載高解析度圖,導致記憶體暴增的解決辦法

五、反思

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

功能擴充套件

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

2. 快取優化

對於快取如何做優化?

  • 增刪查詢的速度
  • 提高命中率

(1). -> LRU -> LRU+FIFO (2). 快取模糊匹配 針對同一圖片,不同大小的請求。如果快取中有更大的圖片,也視為命中快取。

相關文章