- Asynchronous image downloader with cache support as a UIImageView category
*一個異步的圖片下載與緩存的UIImageViewCategory
對于它究竟是如何工作的,相信大家應該或多或少都已經有所了解。但是它內部是怎么實現,又有那些細節,我卻一直犯懶沒有真正的好好去研究過,最近不是很忙,于是我就仔細的研究了一下它的實現細節,這里我源碼的版本為4.0.0,也就是當前最新的版本。
**在這里我推薦大家去github
下載對應的源碼,一邊看blog一邊看對應的源碼,最好再做上自己的注釋,這樣會看的更快,且做做筆記會加深自己的映像,SDWebImage:github
地址:https://github.com/rs/SDWebImage **
好了,廢話不多說,直接來看代碼吧。
下面的代碼是我們經常使用的SDWebImage
的方法之一,給imageView
傳入對應的圖片url和占位圖片,它就幫我們實現了圖片的所有操作。
點進它的具體實現,可以看到它是一個UIImageView
的分類,分類的調用方法如下,我已經給對應的參數做出了對應的翻譯:
/**
* 使用一個url,占位圖片和自定義選項來設置imageView
* 下載是異步且緩存的
* url 圖像的url
* placeholder 占位圖片,初始化時被設置,在請求結束時消失
* options 在下載圖片的時候使用的選項,看SDWebImageOptions有哪些可能的值
* progressBlock 當圖像下載時候調用的block,這個block在一個后臺隊列執行
* completedBlock 當操作結束時調用的block,這個block沒有返回值,把請求到的圖像作為第一個參數,如果發生錯誤的話,第一個參數為空,第二個參數會包含一個NSError對象,第三個參數是一個bool值,指是從本地緩存還是從網絡來重新獲取圖像,第四個參數是圖片原始的url
*/
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
相信大家對上面的參數,并不陌生,即使曾經沒研究過,看到對應的名稱和注釋也能大概猜出它們的作用,這里唯一不太了解的應該是options
的含義了。
options
是一個枚舉,下面是options
對應的值,作用已經添加在注釋中了
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
* 默認情況下,如果一個url在下載的時候失敗了,那么這個url會被加入黑名單并且library不會嘗試再次下載,這個flag會阻止library把失敗的url加入黑名單(簡單來說如果選擇了這個flag,那么即使某個url下載失敗了,sdwebimage還是會嘗試再次下載他.)
*/
SDWebImageRetryFailed = 1 << 0,
/**
* UI交互期間下載
* 導致延遲下載在UIScrollView減速的時候,(也就是你滑動的時候scrollview不下載,你手從屏幕上移走,scrollview開始減速的時候才會開始下載圖片)
*/
SDWebImageLowPriority = 1 << 1,
/**
* 只進行內存緩存,不進行磁盤緩存
*/
SDWebImageCacheMemoryOnly = 1 << 2,
/**
* 這個標志可以漸進式下載,顯示的圖像是逐步在下載(就像你用瀏覽器瀏覽網頁的時候那種圖片下載,一截一截的顯示
*/
SDWebImageProgressiveDownload = 1 << 3,
/**
* 即使圖像緩存,也要遵守HTTP響應緩存控制,如果需要,可以從遠程位置刷新圖像
* 磁盤緩存將由NSURLCache而不是SDWebImage處理,導致輕微的性能降低。
* 這個選項幫助處理在同樣的網絡請求地址下圖片的改變
* 如果刷新緩存的圖像,完成的block會在使用緩存圖像的時候調用,還會在最后的圖像被調用
* 當你不能使你的URL靜態與嵌入式緩存
*/
SDWebImageRefreshCached = 1 << 4,
/**
* 在iOS4以上,如果app進入后臺,也保持下載圖像,這個需要取得用戶權限
* 如果后臺任務過期,操作將被取消
*/
SDWebImageContinueInBackground = 1 << 5,
/**
* 操作cookies存儲在NSHTTPCookieStore通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES
*/
SDWebImageHandleCookies = 1 << 6,
/**
* 允許使用無效的SSL證書
* 用戶測試,生成情況下小心使用
*/
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
/**
* 優先下載
*/
SDWebImageHighPriority = 1 << 8,
/**
* 在加載圖片時加載占位圖。 此標志將延遲加載占位符圖像,直到圖像完成加載。
*/
SDWebImageDelayPlaceholder = 1 << 9,
/**
* 我們通常不調用transformDownloadedImage代理方法在動畫圖像上,大多數情況下會對圖像進行耗損
* 無論什么情況下都使用
*/
SDWebImageTransformAnimatedImage = 1 << 10,
/**
* 圖片在下載后被加載到imageView。但是在一些情況下,我們想要設置一下圖片(引用一個濾鏡或者加入透入動畫)
* 使用這個來手動的設置圖片在下載圖片成功后
*/
SDWebImageAvoidAutoSetImage = 1 << 11,
/**
* 圖像將根據其原始大小進行解碼。 在iOS上,此標記會將圖片縮小到與設備的受限內存兼容的大小。
*/
SDWebImageScaleDownLargeImages = 1 << 12
};
看完上面的枚舉值,大家應該還是不知道有什么作用,沒關系,接著往下看。
繼續往后可以看到,最終它真正調用的是UIView+WebCache.h
的方法,這里就是要詳細講解的第一個方法,在下面的代碼中我已經貼了一些注釋來方便講解:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 獲取可用的operationKey
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 取消該key對應的任務
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 給該視圖的實例對象設置一個屬性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 如果options不為SDWebImageDelayPlaceholder,那么先把placeholder設置到該視圖上
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
// 如果有url,且設置顯示ActivityIndicator,那么顯示
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
// ??這里的operation不是繼承自NSOperation的,我們可以把它看做一個關聯視圖操作的對象,我們稱它為op對象
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
// 圖像下載成功后,移除ActivityIndicator
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
// 如果有image且options為SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 在這里獲取到圖片,且做一些加工的操作
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 如果有image,設置視圖的圖像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 標記設為需要布局
[sself sd_setNeedsLayout];
} else {
// image已經嘗試獲取過了,但是沒有從網絡端獲取到
// 如果options為SDWebImageDelayPlaceholder,當前視圖設置為占位圖片
// 標記設為需要布局
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 有completedBlock且下載finished為yes,將需要的參數傳出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 將現在的op對象加到對應的視圖實例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
// 如果url為空,拋出錯誤
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
下面我們將一步步來解釋這些代碼的含義:
1.首先先獲取validOperationKey
,如果為空,那么就獲取到當前類的名稱,查看UIImageView+WebCache.h
對應的傳入參數,可以發現UIImageView
傳入的對應validOperationKey
為nil
,也就是說默認情況下,如果我們不直接給validOperationKey
賦值,它就為nil
,那么這里獲得的validOperationKey
一般也就是對應類的class
,也就是說如果是UIImageView
調用這個方法,那么對應的validOperationKey
也就是UIImageView
。
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
2.取消該key
對應的任務
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
什么情況,怎么還沒開始做事情就開始取消了?
在這里我們做一個標記,一會來解釋
??標記1:- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
是什么意思,為什么一來就取消,有什么作用?
3.給該視圖的實例對象設置一個屬性,這里的知識是使用了runtime
,如果對runtime
不夠了解的,可以參看資料:讓你快速上手Runtime。
通俗點講:這里的作用就是給UIView
的實例添加了@property (nonatomic, strong) NSString *url;
,只是這個屬性的獲取方式是通過key/value
的方式來獲得的,url
這個value
對應的key
為&imageURLKey
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
4.接下來就是設置placeholder
,如果不想讓SDWebImage
來幫你設置占位圖片,就給它傳入setImageBlock
來自定義設置占位圖片。
// 如果options不為SDWebImageDelayPlaceholder,那么先把placeholder設置到該視圖上
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
這里有兩個需要講解的
-
options & SDWebImageDelayPlaceholder
: &是按位與
舉個例子:a & b a=1 b=2 a== 0000 0001(二進制) b== 0000 0010(二進制) a & b = 0000 0000(二進制)
放在這里就是,如果options
中包含SDWebImageDelayPlaceholder
,那么就不設置占位圖。 -
dispatch_main_async_safe
:這是一個定義的宏
如果當前是主進程,就直接執行block,否則把block放到主進程運行。為什么要判斷是否是主進程?因為iOS上任何UI的操作都在主線程上執行,所以主進程還有一個名字,叫做“UI進程”。
ifndef dispatch_main_async_safe
define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
endif
5.下面的操作是根據url來加載網絡圖片,分為有`url`有值和`url`無值的情況
```obj
if (url) {
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
先來分析
url
無值的情況,也就是上面代碼中的else
,可以很清晰的看到先會調用[self sd_removeActivityIndicator];
,根據名字我們大概能猜到是移除一個ActivityIndicator
,然后會使用完成的block
在主線程拋出一個NSError
對象。-
現在來看
url
有值的情況,首先// 如果有url,且設置顯示ActivityIndicator,那么顯示 if ([self sd_showActivityIndicatorView]) { [self sd_addActivityIndicator]; }
然后通過
SDWebImageManager
的單例對象調用下面的方法,返回了一個名為operation
的id
類型的對象- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock;
先不看這個方法的實現,先猜一猜這個方法是做什么的?
我想你肯定已經猜到了,這個方法就是下載圖片且給UIImageView
設置圖片的方法
現在先來看看這個方法完成的block中的代碼:
__strong __typeof (wself) sself = wself;
// 圖像下載成功后,移除ActivityIndicator
[sself sd_removeActivityIndicator];
// 如果self為nil,直接返回
if (!sself) {
return;
}
然后如果獲取到圖片,options
中包含SDWebImageAvoidAutoSetImage
,且完成的block
不為空的情況下,直接調用完成block
返回
// 如果有image且options為SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 在這里獲取到圖片,且做一些加工的操作
completedBlock(image, error, cacheType, url);
return;
}
如果沒有獲取到options
為SDWebImageAvoidAutoSetImage
,但是獲取到了image
,直接設置對應視圖的image
else if (image) {
// 如果有image,設置視圖的圖像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 標記設為需要布局
[sself sd_setNeedsLayout];
}
然后就是當image
沒有獲取到的時候的操作,如果之前設置的options
有SDWebImageDelayPlaceholder
(也就是延遲加載占位圖),那么現在也應該把占位圖設置上了
else {
// image已經嘗試獲取過了,但是沒有從網絡端獲取到
// 如果options為SDWebImageDelayPlaceholder,當前視圖設置為占位圖片
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
最后,在所有的判斷結束以后,通過completedBlock
將對應的參數傳遞出去
// 有completedBlock且下載finished為yes,將需要的參數傳出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
在url不為nil的邏輯代碼的最后,將前面生成的operation
和最開始獲取到的validOperationKey
設置到對應的視圖,也就是下面的代碼?。?!
在這里我們再做一個標記,下面來解釋
??標記2:- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key
又是什么意思,和上面的標記1有什么關系?
// 將現在的op對象加到對應的視圖實例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上面對對應的邏輯進行大概的梳理,大家應該學習到了一些,但是有些地方肯定還是不清楚,所以看下面吧
下面是解決問題的時間
第一個問題
- 首先看到??標記1和上面的??標記2
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
在所有操作剛開始執行的時候,視圖就執行了個取消的操作,最后又給視圖增加了一個operation
,這到底是怎么回事?
1.根據經驗,如果要給一個UIImageView
設置image
,那么肯定要獲取到對應的image
,如果這是一個網絡圖片,那么肯定是要將這個圖片下載,然后下載好了,再將圖片設置到對應的UIImageView
,相信大家對這個邏輯是沒有異議的。
2.現在下載圖片對應的操作就是id <SDWebImageOperation> operation
來執行,一開始的取消操作就是取消了這樣一個任務
注意:這里的operation
可不是繼承自NSOperation
的對象,而是一個繼承自NSObject的對象,你可以將它看做一個操作圖片更新的對象
3.看一下- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
對應的實現,首先通過[self operationDictionary]
獲取到存有operation
的字典(這里的字典也是通過runtime
動態來添加的),然后通過對應的key
取出對應的operation
,調用cancel
來取消對應的操作,然后通過key
移除對應的operation
。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
// operation的字典
SDOperationsDictionary *operationDictionary = [self operationDictionary];
id operations = operationDictionary[key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
4.接著看一下對應的設置方法,設置方法中先調用了sd_cancelImageLoadOperationWithKey
,然后再將對應的operation
添加到了字典中
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
if (key) {
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self operationDictionary];
operationDictionary[key] = operation;
}
}
}
下面我舉個例子來講一下這么做的作用:
在常用的tableView
中cell
上有圖片是再常見不過的了,如下所示的這種cell
- 在我們使用
SDWebImage
給上面的cell
中的imageview
設置網絡圖片的時候,圖片的下載是異步的,那么如果現在給當前cell設置的為cell.imageview
為a.png
,隨著tableView
的滑動,這個cell
會被復用,復用后現在cell.imageview
為b.png
,這里的a.png
和b.png
都是從網絡上異步下載的,不是本地的資源圖片 - 一開始
cell
的index
為1,image
為a
,復用以后cell
的index
為6,image
為b
,按道理來說圖片應該先為a
,然后為b
,但是a
很大,b
很小,b
都已經下載好了,a
還沒有下載好,當滑動到顯示index
為6的cell
的時候,cell
的圖片先顯示的b
,因為b
已經下載好了,過了一會,a
也下載好了
那么神奇的事情發生了,index
為6的cell
中的圖片a
把b
覆蓋了,應該顯示b
的變成顯示a
了 - 整個數據都亂了,這實在太可怕了
如果上面我舉的例子沒看懂,請反復多看幾遍??!
好,我現在認為你已經看懂了~
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上面??的兩個方法就是為了防止這種情況的發生,因此先取消對應的圖片操作,再重新添加,剛開始先通過key
獲取operation
,如果有operation
對象---->取消。當重新產生一個operation
對象以后,還是看對應的字典中有沒有,有----> 取消(因為現在還沒將新產生的operation
添加到字典中),沒有--->operationDictionary[key] = operation;
,將這個operation
放到字典中,這樣就可以保證一個視圖對象只有一個operation
在操作圖像
在這里也就是說如果設置了cell
的網絡圖片為b
,那么就取消掉之前的a
的相關操作,這樣就不會出現顯示錯亂的問題了。
作者的想法真的是很聰明呀!
第二個問題
在SDWebImage中常??梢钥吹?code>options & SDWebImageRefreshCached這種寫法,查看SDWebImageRefreshCached
的定義可以看到SDWebImageRefreshCached = 1 << 4
。
例如:a=1 b=2 a== 0000 0001(二進制) b== 0000 0010(二進制) a & b = 0000 0000 (二進制) 十進制為0
也就是說SDWebImageRefreshCached
是將1左移4位的一個值,二進制表示為00010000,十進制為16
在接下來的代碼中還會看到downloaderOptions | SDWebImageDownloaderLowPriority
,這種寫法是按位或,也是位運算的一種:
例如:a=5,b=11; 5 ==0000 0101 (二進制) 10==0000 1011(二進制) a | b== 0000 1111(二進制) 十進制為15
如果想了解更多的相關知識,可以參考這篇博客:按位與,按位或
總結
我用了一張流程圖來表示這篇文章的內容,方便大家查看
以上是一些我的個人理解,如果有什么不對的地方也希望大家能夠指出,互相學習!
這是SDWebImage源碼解析的第一篇,下一篇將會對下面產生
operation
的方法進行分析,歡迎大家關注!
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock