通讀SDWebImage②--視圖分類


對於視圖分類,我們最熟悉的當屬`UIImageView+WebCache`這個分類了。通常在為一個UIImageView設置一張網絡圖片並讓SD自動緩存起來就會使用這個分類下的`- (void)sd_setImageWithURL:(NSURL *)url;`方法,如果想要設置占位圖,則使用了可以傳遞占位圖的方法。本文會從這個方法入手介紹一些視圖分類的使用。首先我們要看一下SD中有關視圖的幾個分類: ```objectivec 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進行設置綁定的屬性的時候,可能會將在別的文件中設置的屬性值覆蓋。

UIView+WebCacheOperation這個分類提供了三個方法,用於操作綁定關系。

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

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

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

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

- (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設置圖片網絡的方法: ```objectivec - 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:`方法,只是傳遞的參數不同。(插語:帶方法描述的語言就是麻煩,省略參數做起來都復雜)。

下面我們看一下`- sd_setImageWithURL: placeholderImage: options: progress: completed:`方法的實現:
```objectivec
- (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;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM