通讀SDWebImage②--視圖分類

對於視圖分類,咱們最熟悉的當屬UIImageView+WebCache這個分類了。一般在爲一個UIImageView設置一張網絡圖片並讓SD自動緩存起來就會使用這個分類下的- (void)sd_setImageWithURL:(NSURL *)url;方法,若是想要設置佔位圖,則使用了能夠傳遞佔位圖的方法。本文會從這個方法入手介紹一些視圖分類的使用。首先咱們要看一下SD中有關視圖的幾個分類:web

UIView+WebCacheOperation // 將操做與視圖綁定和取消綁定
UIImageView+WebCache // 對UIImageView設置網絡圖片,實現異步下載、顯示、同時實現緩存
UIImageView+HighlightedWebCache // 與UIImageView+WebCache的功能徹底一致,只是將image設置給UIImageView的highlightedImage屬性而不是image屬性
MKAnnotationView+WebCache // 與UIImageView+WebCache的功能徹底一致,只是將image設置給了MKAnnotationView的image屬性
UIButton+WebCache // 功能很強大,能夠設置不一樣的state的BackgroundImage或者Image

下面咱們先看一下全部的視圖分類都依賴的UIView的分類:UIView+WebCacheOperation
數組

UIView+WebCacheOperation

爲方便找到和管理視圖的正在進行的一些操做,SD將每個視圖的實例和它正在進行的操做(下載和緩存的組合操做)綁定起來,實現操做和視圖的一一對應關係,以即可以隨時拿到視圖正在進行的操做,控制其取消等。緩存

具體的實現是使用runtime給UIView綁定了一個屬性,這個屬性的key是static char loadOperationKey的地址,
這個屬性是NSMutableDictionary類型,value爲操做,key是針對不一樣類型的視圖和不一樣類型的操做設定的字符串網絡

爲何要使用static char loadOperationKey的地址做爲屬性的key,實際上不少第三方框架在給類綁定屬性的時候都會使用這種方案(如AFN),這樣作有如下幾個好處:框架

1.佔用空間小,只有一個字節。
2.靜態變量,地址不會改變,使用地址做爲key老是惟一的且不變的。
3.避免和其餘框架定義的key重複,或者其餘key將其覆蓋的狀況。好比在其餘文件(仍然是UIView的分類)中定義了同名同值的key,使用objc_setAssociatedObject進行設置綁定的屬性的時候,可能會將在別的文件中設置的屬性值覆蓋。iview

UIView+WebCacheOperation這個分類提供了三個方法,用於操做綁定關係。異步

// 返回綁定的屬性
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;

// 對綁定的字典屬性setObject
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;

// 對綁定的字典屬性removeObject
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key;

須要注意對綁定值setObject的時候的一些細節:async

- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
    // 若這個key對應的操做原本就有且正在執行,那麼先將這個操做取消,並將它移除。
    [self sd_cancelImageLoadOperationWithKey:key];
    // 而後設置新的操做
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    [operationDictionary setObject:operation forKey:key];
}

UIImageView+WebCache、UIImageView+HighlightedWebCache、MKAnnotationView+WebCache

這一部雖然標題設置了三個分類,可是咱們主要講解UIImageView+WebCache,在本文的開始就說到,另外兩個分類的實現是徹底一致的,並且代碼重複度爲99%(絲毫沒有誇張)。
UIImageView+WebCache,最熟悉的就是如下幾個爲UIImage設置圖片網絡的方法:動畫

- sd_setImageWithURL:
- sd_setImageWithURL: placeholderImage:
- sd_setImageWithURL: placeholderImage: options:

- sd_setImageWithURL: completed:
- sd_setImageWithURL: placeholderImage: completed:
- sd_setImageWithURL: placeholderImage: options: completed:
- sd_setImageWithURL: placeholderImage: options: progress: completed:

- sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: completed:

但不管是使用哪一個方法,它們的實現上都是調用了- sd_setImageWithURL: placeholderImage: options: progress: completed:方法,只是傳遞的參數不一樣。(插語:帶方法描述的語言就是麻煩,省略參數作起來都複雜)。ui

下面咱們看一下- sd_setImageWithURL: placeholderImage: options: progress: completed:方法的實現:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad]; // 移除UIImageView當前綁定的操做。這一句很是關鍵,當在TableView的cell包含了的UIImageView被重用時,首先調用這一行代碼,保證這個ImageView的下載和緩存組合操做都被取消。若是①上次賦值的圖片正在下載,則下載再也不進行;②下載完成了,但尚未執行到調用回調(回調包含wself.image = image) ,因爲操做被取消,於是不會顯示和重用的cell相同的圖片;③以上兩種狀況只有在網速極慢和手機處理速度極慢的狀況下才會發生,實際上發生的機率很是小,大多數是這種狀況:操做已經進行到下載完成了,此次使用的cell是一個重用的cell,並且保留着imageView的image,對於這種狀況SD會用下面的設置佔位圖的語句,將image暫時設置爲佔位圖,若是佔位圖爲空,就意味着先暫時清空image。
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 將傳入的url與self綁定
    
    // 若是沒有設置延遲加載佔位圖,設置image爲佔位圖
    // 這句代碼要結合上面的理解,實際上在這個地方SD埋了一個bug,若是設置了SDWebImageDelayPlaceholder選項,會忽略佔位圖,而若是imageView在重用的cell中,這時會顯示重用着的image。
    // 我建議將下面的兩句改成
    /*
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                self.image = placeholder;
            });
        } else {
            dispatch_main_async_safe(^{
                self.image = nil;
            });
        }
    */    
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    if (url) {
        // 檢查是否經過`setShowActivityIndicatorView:`方法設置了顯示正在加載指示器。若是設置了,使用`addActivityIndicator`方法向self添加指示器
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }
        
        __weak __typeof(self) wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            [wself removeActivityIndicator]; // 移除加載指示器
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                { // 若是設置了禁止自動設置image選項,則不會執行`wself.image = image;`,而是直接執行完成回調,有用戶本身決定如何處理。
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    // 設置image
                    wself.image = image;
                    [wself setNeedsLayout];
                } else { // image爲空,而且設置了延遲設置佔位圖,會將佔位圖設置爲最終的image
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) { 
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 爲UIImageView綁定新的操做
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else { // 判斷url不存在,移除加載指示器,執行完成回調,傳遞錯誤信息。
        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);
            }
        });
    }
}

這個就是完整的加載網絡圖片的過程,而具體的如何實現下載細節、網絡訪問驗證、在下載完成以後如何進行內存和磁盤緩存的,請參照上一篇文章的內容。

上面的全部的爲UIImageView設置網絡圖片的方法中有一個和其餘稍微不一樣的- sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: completed:,其實也就是張的有點不一樣,它的實現是這樣的:

- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
    UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:key];
    
    [self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];    
}

能夠看到,它的思路是先取得上次緩存的圖片,而後做爲佔位圖的參數再次進行一次圖片設置。

在設置圖片的過程當中,有關如何移除和添加加載指示器的兩個方法,咱們這裏不作討論,實際上是對系統的UIActivityIndicatorView視圖的使用。

還有一個須要的方法- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs,要注意的是這個方法傳遞的參數是一個由URL組成的數組,這個方法用來設置UIImage的animationImages屬性。它的實現思路是:
將遍歷URL數組中的元素,根據每一個URL建立一個下載操做並執行,在回調裏面對imationImages屬性值追加下載好的image。它的具體實現以下:

- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs {
    [self sd_cancelCurrentAnimationImagesLoad];
    __weak __typeof(self)wself = self;

    NSMutableArray *operationsArray = [[NSMutableArray alloc] init];

    for (NSURL *logoImageURL in arrayOfURLs) {
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:logoImageURL options:0 progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            if (!wself) return;
            dispatch_main_sync_safe(^{
                __strong UIImageView *sself = wself;
                [sself stopAnimating]; // 先動畫中止
                if (sself && image) {
                    NSMutableArray *currentImages = [[sself animationImages] mutableCopy];
                    if (!currentImages) {
                        currentImages = [[NSMutableArray alloc] init];
                    }
                    [currentImages addObject:image]; // 追加新下載的image

                    sself.animationImages = currentImages;
                    [sself setNeedsLayout];
                }
                [sself startAnimating];
            });
        }];
        [operationsArray addObject:operation];
    }
    // 注意這裏綁定的不是單個操做,而是操做數據。UIView+WebCacheOperation的方法`sd_cancelImageLoadOperationWithKey:`也對操做數組作了適配
    [self sd_setImageLoadOperation:[NSArray arrayWithArray:operationsArray] forKey:@"UIImageViewAnimationImages"];
}

UIButton+WebCache

有關UIButton+WebCache分類中的方功能確實強大:能夠爲image的不一樣state(Normal、Highlighted、Disabled、Selected)設置不一樣的backgoud圖片或者image圖片,可是它的實現很簡單,幾乎和上面介紹的UIImageView的設置方法是相同的,只是UIButton多了一個管理不一樣state下的url的功能。

UIButton管理圖片的url其實也是經過runtime綁定屬性來實現的,和UIImageView不一樣的是:UIImageView只需一張圖片因此就綁定了NSURL類型值,而UIButton須要多張圖片且要區分state,因此使用NSMutableDictionary來存儲圖片的URL,其中key是@(state),value是該state對應的圖片的url。須要注意的是它只是存儲了image的URL,而並無存儲backgroudImage的URL。

- (NSMutableDictionary *)imageURLStorage {
    NSMutableDictionary *storage = objc_getAssociatedObject(self, &imageURLStorageKey);
    if (!storage)
    {
        storage = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &imageURLStorageKey, storage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    return storage;
}

- sd_setImageWithURL: forState: placeholderImage: options: completed:對它的調用:

[self.imageURLStorage removeObjectForKey:@(state)];

self.imageURLStorage[@(state)] = url;
相關文章
相關標籤/搜索