SDWebImage探究(八) —— 深入研究圖片下載流程(二)之開始下載并返回下載結(jié)果的總的方法

版本記錄

版本號 時間
V1.0 2018.02.11

前言

我們做APP,文字和圖片是絕對不可缺少的元素,特別是圖片一般存儲在圖床里面,一般公司可以委托第三方保存,NB的公司也可以自己存儲圖片,ios有很多圖片加載的第三方框架,其中最優(yōu)秀的莫過于SDWebImage,它幾乎可以滿足你所有的需求,用了好幾年這個框架,今天想總結(jié)一下。感興趣的可以看其他幾篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 圖片類型判斷深入研究
7. SDWebImage探究(七) —— 深入研究圖片下載流程(一)之有關(guān)option的位移枚舉的說明

頭文件的引入

如果你的空間是UIIMageView,那么需要引入的頭文件時#import "UIImageView+WebCache.h";但是如果是UIButton,那么需要引入的頭文件是#import "UIButton+WebCache.h"。這里就以UIIMageView為例就行說明,UIButton那個是類似的。


下載接口

SDWebImage為下載圖片提供了很多接口,一共如下所示:

- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(nullable NSURL *)url
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

這里大家可以看到:

  • 上面7個接口都可以下載圖片,且都是異步的。
  • 第一個方法最簡單,只需要一個url地址;最后一個是5個參數(shù),是條件最多的方法,大家可以根據(jù)需要選擇需要的方法,一般我們選擇第2個方法的情況居多。
  • 不管你用的哪個方法,最后在代碼的實現(xiàn)上,你都是調(diào)用的是最后一個包含5個參數(shù)的那個方法,只不過沒有的參數(shù)傳為了nil或者0,比如方法1的實現(xiàn)如下所示。
- (void)sd_setImageWithURL:(nullable NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
  • 對于包含5個參數(shù)的最后那個方法,需要重點注意的就是options那個參數(shù),這里是一個枚舉,很多東西都可以在里面設(shè)置,當(dāng)你調(diào)用方法1的時候,options默認(rèn)傳0,下面我們就看一下0是什么意思。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
     * This flag disable this blacklisting.
     */
    SDWebImageRetryFailed = 1 << 0,

    /**
     * By default, image downloads are started during UI interactions, this flags disable this feature,
     * leading to delayed download on UIScrollView deceleration for instance.
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * This flag disables on-disk caching
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * This flag enables progressive download, the image is displayed progressively during download as a browser would do.
     * By default, the image is only displayed once completely downloaded.
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
     * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
     * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
     * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
     *
     * Use this flag only if you can't make your URLs static with embedded cache busting parameter.
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * Enable to allow untrusted SSL certificates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * By default, images are loaded in the order in which they were queued. This flag moves them to
     * the front of the queue.
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * We usually don't call transformDownloadedImage delegate method on animated images,
     * as most transformation code would mangle it.
     * Use this flag to transform them anyway.
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * By default, image is added to the imageView after download. But in some cases, we want to
     * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
     * Use this flag if you want to manually set the image in the completion when success
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /**
     * By default, images are decoded respecting their original size. On iOS, this flag will scale down the
     * images to a size compatible with the constrained memory of devices.
     * If `SDWebImageProgressiveDownload` flag is set the scale down is deactivated.
     */
    SDWebImageScaleDownLargeImages = 1 << 12
};

這里0的意思就是SDWebImageRetryFailed,它的意思是默認(rèn)情況下,當(dāng)下載失敗后就會將url放入黑名單不會再次下載了,這里傳0,就是默認(rèn)不成立,意思就是下載失敗還是要繼續(xù)下載的,其他的options大家都需要重點看一下,后面涉及的時候會和大家接著說。


調(diào)用接口后的第一個方法

上面調(diào)用完接口后,我們看框架調(diào)用了這個方法。作者給放入在#import "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

這個和上面的方法很相似,不同的是sd_internalSetImageWithURL,可能是作者將其作為內(nèi)部方法加入的internal作為區(qū)別。

- (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 {
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    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);
            }
        });
    }
}

下面我們就看一下這個方法里面都做了什么?

1. 取消對應(yīng)key正在下載的圖片

我們看一下最前面的三行代碼

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

這里手下你是獲取validOperationKey的值,它是根據(jù)方法中的參數(shù)operationKey進(jìn)行確定的,如果你插件接口,就會發(fā)現(xiàn),每次你調(diào)用下載接口這個operationKey傳入的都是nil,這里用一個三目運算符,如果為nil,那么就把值NSStringFromClass([self class]賦給它,這里是用UIImageView調(diào)用的,所以validOperationKey的值就是UIImageView

然后執(zhí)行第二句[self sd_cancelImageLoadOperationWithKey:validOperationKey];取消對應(yīng)的key的圖像下載。具體如何取消下載的,我們會單獨發(fā)文進(jìn)行說明,這里限于篇幅,就先寫這么多了。

繼續(xù)看下面這個objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);這個是運行時的比較典型的應(yīng)用,因為分類是不能利用setter或者getter。這個時候我們就需要利用運行時,將key對應(yīng)的值綁定到當(dāng)前對象中,當(dāng)我們想用的時候也是根據(jù)key和當(dāng)前對象或者綁定的值。

我們先看一下API

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

然后在看一下作者要使用的地方。

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

上面這個就是將值url綁定到對象self中,key是imageURLKey

- (nullable NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, &imageURLKey);
}

上面這個就是根據(jù)key imageURLKey獲取和self綁定的值。我們通過控制臺輸出如下:

(lldb) po objc_getAssociatedObject(self, &imageURLKey);
http://image.xxxx.com/6e51869946890531e1b24012a9b489ea-100_100.jpg

這里作者就將key將url綁定到self中,所以利用此方法獲取的也是url的值,也就是圖像的下載地址。這樣以后我們在這個文件中想獲取到圖像的下載地址就很方便了,直接調(diào)用這個方法就可以,達(dá)到了一般類中類似屬性或者成員變量的那種全局的效果。具體這么做有什么用,我后面會另外分一個篇幅進(jìn)行說明。

2. 與options相關(guān)的邏輯處理

我們先看一下這段代碼

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

這里要先看一下枚舉值SDWebImageDelayPlaceholder

    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

這個枚舉值的意思是,默認(rèn)下,當(dāng)圖像下載的時候顯示占位圖,一旦設(shè)置這個option,意思就是在圖像下載之前,不顯示占位圖。我們接著看,到這里,如果你調(diào)用下載圖片的接口的時候如果傳入了options這個枚舉參數(shù),那么這里就進(jìn)行了對比,如果不是SDWebImageDelayPlaceholder值,那么就調(diào)用方法- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock,進(jìn)行設(shè)置占位圖。如果是SDWebImageDelayPlaceholder值,那么這個if里面就不會執(zhí)行,也就是說不會設(shè)置占位圖。

這里還有一個地方值得我們?nèi)W(xué)習(xí),就是這種帶參數(shù)的宏定義。

dispatch_main_async_safe

這里是按照下面這個進(jìn)行定義的。

#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);\
    }

這里面進(jìn)行了判斷,strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0,經(jīng)過判斷值為0表示相等,就是主線程,執(zhí)行block。每個線程我們create以后都會分配給一個可以區(qū)分的label,是一個字符串,通過傳入DISPATCH_CURRENT_QUEUE_LABEL查詢當(dāng)前線程的label值,所以這個就很好理解了,通過主線程的label與當(dāng)前線程的label進(jìn)行對比,如果相等,就執(zhí)行block,如果不相等,就直接在主線程執(zhí)行。下面我們看一個函數(shù)const char * dispatch_queue_get_label(dispatch_queue_t _Nullable queue);,可以幫助大家更好的理解這個函數(shù)。

/*!
 * @function dispatch_queue_get_label
 *
 * @abstract
 * Returns the label of the given queue, as specified when the queue was
 * created, or the empty string if a NULL label was specified.
 *
 * Passing DISPATCH_CURRENT_QUEUE_LABEL will return the label of the current
 * queue.
 *
 * @param queue
 * The queue to query, or DISPATCH_CURRENT_QUEUE_LABEL.
 *
 * @result
 * The label of the queue.
 */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_PURE DISPATCH_WARN_RESULT DISPATCH_NOTHROW
const char *
dispatch_queue_get_label(dispatch_queue_t _Nullable queue);

3. 與url相關(guān)的if - else邏輯處理

上面執(zhí)行完畢,接下來就是后面的與url值相關(guān)的if- else邏輯處理,可以看見,處理完畢了,該方法也就結(jié)束了,可以預(yù)見這里面的邏輯嵌套的應(yīng)該很復(fù)雜。

  • 當(dāng)url存在不為空

1)首先利用SDWebImageManager單利進(jìn)行下載任務(wù)。

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock 

這個方法是異步的,當(dāng)返回成功后,就會在主線程進(jìn)行不同條件的標(biāo)記刷新和返回對應(yīng)需要的數(shù)據(jù)。

2)成功返回后,先移除加載的動畫,這里做了容錯的處理。

 __weak __typeof(self) wself = self;

... ... 
//結(jié)果返回中
__strong __typeof (wself) sself = wself;
if (!sself) {
    return;
}

這樣的處理是需要我們多學(xué)習(xí)和加入到我們的代碼開發(fā)中的。

接著我們看在主線程中都做了什么,其實就是下面這個多條件分支的代碼。

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);
}

我們一起開看一下。

a)如果返回的image不為空,并且option是SDWebImageAvoidAutoSetImage且完成的completedBlock不為空時,返回的是completedBlock(image, error, cacheType, url);,給調(diào)用接口進(jìn)行使用。這里的SDWebImageAvoidAutoSetImage的定義如下所示,其實就是下載后不要自動給UIImageView賦值image,這里選擇的就是手動。

    /**
     * By default, image is added to the imageView after download. But in some cases, we want to
     * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
     * Use this flag if you want to manually set the image in the completion when success
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,

b)如果只有image不為空,其他幾個條件最少有一個不滿足的時候,就會走到下一個分支,這里就是直接將下載后的圖片給UIImageView,并且標(biāo)記為需要刷新。

[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];

這里傳入的setImageBlock為nil,所以這里就是單純的賦值和刷新界面。

c) 以上條件都不滿足的情況下,這個時候判斷options。如果options是SDWebImageDelayPlaceholder也就是延遲顯示占位圖的情況,那么就是調(diào)用

[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];

調(diào)用和上面相同的方法顯示占位圖,這里給UIImageView傳入的就是placeholder,并標(biāo)記為需要刷新。

d)單獨的一個判斷,如果completedBlock不為空以及 finished == YES的情況下,那么就返回completedBlock(image, error, cacheType, url),這時候返回的image有可能是空的。

e)根據(jù)指定的validOperationKey綁定這個下載的operation

這里就是一句代碼

[self sd_setImageLoadOperation:operation forKey:validOperationKey];

這里為什么要綁定,后面會分開一篇文章進(jìn)行講解。大家先記住,下載成功回調(diào)綁定了這個下載操作。

  • 如果url為空,在主線程中進(jìn)行了操作。
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);
    }
});

這里做的首先是移除下載的動畫,并進(jìn)行判斷completedBlock不為nil的時候直接返回completedBlock(nil, error, SDImageCacheTypeNone, url);,這里的block的image為nil,并給出了一個error。

這里錯誤error的錯誤域是:

NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";

就是一個常量的字符串。

錯誤碼就是-1了,緩存類型為SDImageCacheTypeNone,既然nil為空沒有下載下來圖片,當(dāng)然就不會有緩存了。

到此為止,下載圖片的第一個方法就解析完了,大家覺得簡單嗎?不,還有很多沒和大家說,包括如何進(jìn)行下載的,下載后的解碼以及緩存等很多細(xì)節(jié)都是要進(jìn)行詳細(xì)解析的,這里只是給大家一個基本的流程和概念,后面會分幾篇進(jìn)行詳細(xì)的說明。一定會給大家講的清楚和明白。

后記

本篇已結(jié)束,后面更精彩~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容