前言
開發(fā)iOS有一段時間了蜗细,平時工作中主要還是完成業(yè)務功能蜈项。類似網(wǎng)絡請求烙无,圖片加載等等都直接使用現(xiàn)成的開源類庫名党,項目主要還是以穩(wěn)定為先。
但長期這樣感覺難以進步质蕉,想要進階除了看書外就得多看看開源類庫的源碼了势篡。
于是就從SDWebImage入手,在深入學習后發(fā)現(xiàn)它的代碼各層職責分工明確模暗,代碼量也不是很多禁悠,利用業(yè)余時間斷斷續(xù)續(xù)學習花費了大約三周時間,感覺比較適合作為第一個供學習的開源類庫兑宇。
大致涉及到的知識點:
- Block
- GCD
- NSOperation
- Associated Objects
- NSURLRequest
- NSCache
- 圖片類型識別與處理
文章中難免出現(xiàn)問題碍侦,望各位給予糾正,有問題歡迎一起討論隶糕。
源碼分析
SDWebImage使用起來非常簡單瓷产,只需調(diào)用sd_setImageWithURL
方法,就可以將圖片異步的加載并顯示在UIImageView上。
所以接下來我們就從sd_setImageWithURL
開始說起:
NSURL * url = [NSURL URLWithString:@"http://hbimg.b0.upaiyun.com/ddd2cee8ff21d4a09a86b68972b78b15ba7bc2a035fa4-sGYzEJ_fw658"];
[imageView sd_setImageWithURL:url];
上面代碼所使用的是sd_setImageWithURL
最簡單的版本枚驻,我們跟進去看一下濒旦,發(fā)現(xiàn)方法里其實幫我們設置好了默認參數(shù),最終調(diào)用到的是另一個方法:
- (void)sd_setImageWithURL:(NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
我們跟進去看看再登,通過注釋可以得知這個方法的用途:
/**
* ?根據(jù)url給imageView設置image尔邓,占位圖和各種自定義設置
*
* 使用異步下載和緩存
*
* @param url 圖片的url
* @param placeholder 占位圖
* @param options 下載圖片時的各種設置. @see SDWebImageOptions.
* @param progressBlock 當圖片正在下載時會被回調(diào)到
* @param completedBlock 當任務完成時會被回調(diào)到 。該block沒有返回值使用UIImage作為第一個參數(shù)
* 如果下載中出現(xiàn)錯誤UIIMage為nil并且第二個參數(shù)會包含NSError
* 第三個參數(shù)是一個枚舉(*原注釋這塊寫的是布爾值)霎冯,表示圖片是從本地緩存中還是網(wǎng)絡中取回的
* 第四個參數(shù)是原生的image url
*/
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
//completedBlock铃拇,參數(shù)與注釋對應
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
接著我們看代碼钞瀑,然后一步步分析:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
//取消當前UIImageView正在加載的圖片任務
[self sd_cancelCurrentImageLoad];
//相當于給當前UIImageView對象上綁定圖片url屬性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果options中沒有傳入SDWebImageDelayPlaceholder參數(shù)沈撞,則設置占位圖
//這里出現(xiàn)了dispatch_main_async_safe,其實是SDWebImage定義的宏,其實就是將UI操作放入主線程中用的
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// 檢查是否打開了"會轉(zhuǎn)動的菊花"選項
if ([self showActivityIndicatorView]) {
[self addActivityIndicator]; //< 界面上會出現(xiàn)轉(zhuǎn)動的菊花
}
__weak __typeof(self)wself = self;
//從方法名中可以猜出它是用來下載圖片用的雕什,目前只需要這么理解就好缠俺,后面章節(jié)會具體談到
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
[wself removeActivityIndicator]; //<將轉(zhuǎn)動的菊花從界面上移除
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
//設置了SDWebImageAvoidAutoSetImage參數(shù)時显晶,默認不會將image添加進UIViewImage對象,而是放置到completedBlock中交由調(diào)用方自己處理壹士,比如做個濾鏡或者添加淡出淡入效果什么的
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image; //< 設置image
[wself setNeedsLayout];
} else { //< 當image為nil
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;//< 此時再將占位圖設置進去
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
//保存本次operation磷雇,如果發(fā)生多次圖片請求加載可以用來取消
//先取消當前UIImageView正在加載的任務,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
這里先提幾個點:
1.在代碼中我們會發(fā)現(xiàn)有dispatch_main_async_safe
這么一個神奇的東東躏救,其實它是SDWebImage定義的宏唯笙,將UI操作放入主線程中用的:
#define dispatch_main_async_safe(block)
if ([NSThread isMainThread]) { //< 如果當前在主線程中
block();
} else { //< 不在主線程就將它放入主線程
dispatch_async(dispatch_get_main_queue(), block);
}
2.代碼中偶爾會出現(xiàn)objc_setAssociatedObject
,簡單的說使用該技巧可以很方便的將變量動態(tài)綁定在該實例下盒使,原因在于Category中是不允許添加實例變量崩掘。
回到主題來,代碼在請求下載圖片前執(zhí)行了[self sd_cancelCurrentImageLoad]
少办,從方法名上可以猜出它的大意“取消當前圖片的加載”苞慢,他是作什么用的呢,為什么在加載圖片前會需要用到取消這么一個方法英妓?帶著疑問我們繼續(xù)挽放,發(fā)現(xiàn)調(diào)用了另一個方法,看來這里只負責傳入對應的“key”
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
再跟進來我們可以看到具體的實現(xiàn)了
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
//利用AssociatedObject維護的字典蔓纠,用于存放當前任務中的operation(圖片請求)
NSMutableDictionary *operationDictionary = [self operationDictionary];
//key為"UIImageViewImageLoad"
id operations = [operationDictionary objectForKey:key];
if (operations) { //< 當前有正在執(zhí)行的operation辑畦,需要取消任務
//多個operation的是gif(多幀),單個的是普通圖片
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]; //< 取消
}
//刪除對應key的對象
//每次對應UIView有一個圖片請求的任務時腿倚,都會設置對應的key航闺,所以可以根據(jù)這個key來判斷是否有正在執(zhí)行的任務
[operationDictionary removeObjectForKey:key];
}
}
看完上面這段代碼后,我們大致有了一個概念猴誊,同時也發(fā)現(xiàn)這兩段代碼的“key”是一樣的:
//取消當前UIImageView正在加載的圖片任務
[self sd_cancelCurrentImageLoad];
...
//保存本次operation潦刃,如果發(fā)生多次圖片請求加載可以用來取消
//先取消當前UIImageView正在加載的任務,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
再回到剛才的疑問懈叹,舉個例子來說就能明白方法的意圖和具體流程了:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/1.png"] placeholderImage:nil];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/2.png"] placeholderImage:nil];
一個imageView請求了兩張圖片乖杠,1.png 和 2.png,但我們只希望顯示 2.png澄成,所以需要取消 1.png的請求胧洒。原因有兩點:
1.在異步請求中(先后順序不定),有可能 1.png 會在 2.png 后面獲取到墨状,會覆蓋掉2.png
2.減少網(wǎng)絡請求卫漫,網(wǎng)絡請求是一個很耗時的操作