原始碼閱讀:AFNetworking(十二)——UIButton+AFNetworking

堯少羽發表於2018-03-15

該文章閱讀的AFNetworking的版本為3.2.0。

這個分類是為UIButton新增非同步載入網路圖片的方法

1.介面檔案

  • 圖片下載器的訪問方法
/**
 設定用於下載圖片的圖片下載器
*/
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader;

/**
 獲取用於下載圖片的圖片下載器
 */
+ (AFImageDownloader *)sharedImageDownloader;
複製程式碼
  • 為按鈕設定圖片的方法
/**
 為按鈕設定指定狀態和指定圖片連結的圖片
 */
- (void)setImageForState:(UIControlState)state
                 withURL:(NSURL *)url;

/**
 為按鈕設定指定狀態、指定圖片連結和指定佔點陣圖的圖片
 */
- (void)setImageForState:(UIControlState)state
                 withURL:(NSURL *)url
        placeholderImage:(nullable UIImage *)placeholderImage;

/**
 為按鈕設定指定狀態、指定圖片連結、指定佔點陣圖以及指定成功與失敗回撥block的圖片
 */
- (void)setImageForState:(UIControlState)state
          withURLRequest:(NSURLRequest *)urlRequest
        placeholderImage:(nullable UIImage *)placeholderImage
                 success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                 failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
複製程式碼
  • 為按鈕設定背景圖片的方法
/**
 為按鈕設定指定狀態和指定圖片連結的背景圖片
 */
- (void)setBackgroundImageForState:(UIControlState)state
                           withURL:(NSURL *)url;

/**
 為按鈕設定指定狀態、指定圖片連結和指定佔點陣圖的背景圖片
 */
- (void)setBackgroundImageForState:(UIControlState)state
                           withURL:(NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholderImage;

/**
 為按鈕設定指定狀態、指定圖片連結、指定佔點陣圖以及指定成功與失敗回撥block的背景圖片
 */
- (void)setBackgroundImageForState:(UIControlState)state
                    withURLRequest:(NSURLRequest *)urlRequest
                  placeholderImage:(nullable UIImage *)placeholderImage
                           success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                           failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
複製程式碼
  • 取消圖片載入方法
/**
 取消按鈕在指定狀態下的所有圖片下載任務
 */
- (void)cancelImageDownloadTaskForState:(UIControlState)state;

/**
 取消按鈕在指定狀態下的所有背景圖片下載任務
 */
- (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state;
複製程式碼

2.實現檔案

2.1.UIButton+_AFNetworking私有分類

2.1.1.image相關

  • 靜態字元
// 普通
static char AFImageDownloadReceiptNormal;
// 高亮
static char AFImageDownloadReceiptHighlighted;
// 已選
static char AFImageDownloadReceiptSelected;
// 禁用
static char AFImageDownloadReceiptDisabled;
複製程式碼
  • 靜態方法
/**
 這個方法通過傳入的控制元件狀態返回對應的靜態字元
 */
static const char * af_imageDownloadReceiptKeyForState(UIControlState state) {
    switch (state) {
        case UIControlStateHighlighted:
            return &AFImageDownloadReceiptHighlighted;
        case UIControlStateSelected:
            return &AFImageDownloadReceiptSelected;
        case UIControlStateDisabled:
            return &AFImageDownloadReceiptDisabled;
        case UIControlStateNormal:
        default:
            return &AFImageDownloadReceiptNormal;
    }
}
複製程式碼
  • 屬性的訪問方法
/**
 通過Runtime的關聯物件為分類新增屬性的getter
 */
- (AFImageDownloadReceipt *)af_imageDownloadReceiptForState:(UIControlState)state {
    return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, af_imageDownloadReceiptKeyForState(state));
}

/**
 通過Runtime的關聯物件為分類新增屬性的setter
 */
- (void)af_setImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt
                           forState:(UIControlState)state
{
    objc_setAssociatedObject(self, af_imageDownloadReceiptKeyForState(state), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製程式碼

2.1.2.backgroundImage相關

  • 靜態字元
// 普通
static char AFBackgroundImageDownloadReceiptNormal;
// 高亮
static char AFBackgroundImageDownloadReceiptHighlighted;
// 已選
static char AFBackgroundImageDownloadReceiptSelected;
// 禁用
static char AFBackgroundImageDownloadReceiptDisabled;
複製程式碼
  • 靜態方法
/**
 這個方法通過傳入的控制元件狀態返回對應的靜態字元
 */
static const char * af_backgroundImageDownloadReceiptKeyForState(UIControlState state) {
    switch (state) {
        case UIControlStateHighlighted:
            return &AFBackgroundImageDownloadReceiptHighlighted;
        case UIControlStateSelected:
            return &AFBackgroundImageDownloadReceiptSelected;
        case UIControlStateDisabled:
            return &AFBackgroundImageDownloadReceiptDisabled;
        case UIControlStateNormal:
        default:
            return &AFBackgroundImageDownloadReceiptNormal;
    }
}
複製程式碼
  • 屬性的訪問方法
/**
 通過Runtime的關聯物件為分類新增屬性的getter
 */
- (AFImageDownloadReceipt *)af_backgroundImageDownloadReceiptForState:(UIControlState)state {
    return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, af_backgroundImageDownloadReceiptKeyForState(state));
}

/**
 通過Runtime的關聯物件為分類新增屬性的setter
 */
- (void)af_setBackgroundImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt
                                     forState:(UIControlState)state
{
    objc_setAssociatedObject(self, af_backgroundImageDownloadReceiptKeyForState(state), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製程式碼

2.2.方法實現

  • 屬性的訪問方法
/**
 通過Runtime的關聯物件為分類新增sharedImageDownloader屬性的getter
 */
+ (AFImageDownloader *)sharedImageDownloader {
    return objc_getAssociatedObject(self, @selector(sharedImageDownloader)) ?: [AFImageDownloader defaultInstance];
}

/**
 通過Runtime的關聯物件為分類新增sharedImageDownloader屬性的setter
 */
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader {
    objc_setAssociatedObject(self, @selector(sharedImageDownloader), imageDownloader, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製程式碼
  • image相關介面方法實現
- (void)setImageForState:(UIControlState)state
                 withURL:(NSURL *)url
{
    // 呼叫下面的方法
    [self setImageForState:state withURL:url placeholderImage:nil];
}

- (void)setImageForState:(UIControlState)state
                 withURL:(NSURL *)url
        placeholderImage:(UIImage *)placeholderImage
{
    // 呼叫下面的方法
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];

    [self setImageForState:state withURLRequest:request placeholderImage:placeholderImage success:nil failure:nil];
}

- (void)setImageForState:(UIControlState)state
          withURLRequest:(NSURLRequest *)urlRequest
        placeholderImage:(nullable UIImage *)placeholderImage
                 success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                 failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure
{
    // 如果這個網路請求正在進行中就不重複執行了
    if ([self isActiveTaskURLEqualToURLRequest:urlRequest forState:state]) {
        return;
    }

    // 取消掉這個狀態的圖片下載任務
    [self cancelImageDownloadTaskForState:state];

    // 例項化圖片下載器
    AFImageDownloader *downloader = [[self class] sharedImageDownloader];
    // 獲取到圖片快取物件
    id <AFImageRequestCache> imageCache = downloader.imageCache;

    // 獲取到快取圖片
    UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil];
    // 如果有快取
    if (cachedImage) {
        // 如果設定了成功回撥block就呼叫,否則就為按鈕設定圖片
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            [self setImage:cachedImage forState:state];
        }
        [self af_setImageDownloadReceipt:nil forState:state];
    // 如果沒有快取
    } else {
        // 如果有佔點陣圖先設定佔點陣圖
        if (placeholderImage) {
            [self setImage:placeholderImage forState:state];
        }

        // 開始下載
        __weak __typeof(self)weakSelf = self;
        NSUUID *downloadID = [NSUUID UUID];
        AFImageDownloadReceipt *receipt;
        receipt = [downloader
                   downloadImageForURLRequest:urlRequest
                   withReceiptID:downloadID
                   success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) {
                       // 如果是當前按鈕的下載回撥
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       if ([[strongSelf af_imageDownloadReceiptForState:state].receiptID isEqual:downloadID]) {
                            // 如果設定了成功回撥block就呼叫,否則就為按鈕設定圖片
                           if (success) {
                               success(request, response, responseObject);
                           } else if(responseObject) {
                               [strongSelf setImage:responseObject forState:state];
                           }
                           // 將儲存下載封裝物件的屬性置nil
                           [strongSelf af_setImageDownloadReceipt:nil forState:state];
                       }

                   }
                   failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
                       // 如果是當前按鈕的下載回撥
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       if ([[strongSelf af_imageDownloadReceiptForState:state].receiptID isEqual:downloadID]) {
                           // 失敗回撥block
                           if (failure) {
                               failure(request, response, error);
                           }
                           // 將儲存下載封裝物件的屬性置nil
                           [strongSelf  af_setImageDownloadReceipt:nil forState:state];
                       }
                   }];

        // 用分類的屬性儲存圖片下載封裝物件
        [self af_setImageDownloadReceipt:receipt forState:state];
    }
}

- (void)cancelImageDownloadTaskForState:(UIControlState)state { 
    // 通過狀態獲取到圖片下載封裝物件
    AFImageDownloadReceipt *receipt = [self af_imageDownloadReceiptForState:state];
    if (receipt != nil) {
        // 根據獲取到的圖片下載封裝物件取消對應的圖片下載任務
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:receipt];
        // 把儲存圖片下載封裝物件的屬性置nil
        [self af_setImageDownloadReceipt:nil forState:state];
    }
}
複製程式碼
  • backgroundImage相關介面方法實現

backgroundImage的實現和image的實現除了在設定圖片時一個給backgroundImage,一個給image之外都相同。

- (void)setBackgroundImageForState:(UIControlState)state
                           withURL:(NSURL *)url
{
    [self setBackgroundImageForState:state withURL:url placeholderImage:nil];
}

- (void)setBackgroundImageForState:(UIControlState)state
                           withURL:(NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholderImage
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];

    [self setBackgroundImageForState:state withURLRequest:request placeholderImage:placeholderImage success:nil failure:nil];
}

- (void)setBackgroundImageForState:(UIControlState)state
                    withURLRequest:(NSURLRequest *)urlRequest
                  placeholderImage:(nullable UIImage *)placeholderImage
                           success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                           failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure
{
    if ([self isActiveBackgroundTaskURLEqualToURLRequest:urlRequest forState:state]) {
        return;
    }

    [self cancelBackgroundImageDownloadTaskForState:state];

    AFImageDownloader *downloader = [[self class] sharedImageDownloader];
    id <AFImageRequestCache> imageCache = downloader.imageCache;

    //Use the image from the image cache if it exists
    UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil];
    if (cachedImage) {
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            [self setBackgroundImage:cachedImage forState:state];
        }
        [self af_setBackgroundImageDownloadReceipt:nil forState:state];
    } else {
        if (placeholderImage) {
            [self setBackgroundImage:placeholderImage forState:state];
        }

        __weak __typeof(self)weakSelf = self;
        NSUUID *downloadID = [NSUUID UUID];
        AFImageDownloadReceipt *receipt;
        receipt = [downloader
                   downloadImageForURLRequest:urlRequest
                   withReceiptID:downloadID
                   success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) {
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       if ([[strongSelf af_backgroundImageDownloadReceiptForState:state].receiptID isEqual:downloadID]) {
                           if (success) {
                               success(request, response, responseObject);
                           } else if(responseObject) {
                               [strongSelf setBackgroundImage:responseObject forState:state];
                           }
                           [strongSelf af_setBackgroundImageDownloadReceipt:nil forState:state];
                       }

                   }
                   failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       if ([[strongSelf af_backgroundImageDownloadReceiptForState:state].receiptID isEqual:downloadID]) {
                           if (failure) {
                               failure(request, response, error);
                           }
                           [strongSelf  af_setBackgroundImageDownloadReceipt:nil forState:state];
                       }
                   }];

        [self af_setBackgroundImageDownloadReceipt:receipt forState:state];
    }
}

- (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state {
    AFImageDownloadReceipt *receipt = [self af_backgroundImageDownloadReceiptForState:state];
    if (receipt != nil) {
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:receipt];
        [self af_setBackgroundImageDownloadReceipt:nil forState:state];
    }
}
複製程式碼
  • 私有方法
/**
 判斷圖片下載請求是否已經正在進行中
 */
- (BOOL)isActiveTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest forState:(UIControlState)state {
    // 先根據狀態獲取到屬性中儲存的圖片下載封裝物件
    AFImageDownloadReceipt *receipt = [self af_imageDownloadReceiptForState:state];
    // 根據兩者的連結是否相同進行比較
    return [receipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString];
}

/**
 判斷背景圖片下載請求是否已經正在進行中
 */
- (BOOL)isActiveBackgroundTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest forState:(UIControlState)state {
    // 實現和上面的方法相同
    AFImageDownloadReceipt *receipt = [self af_backgroundImageDownloadReceiptForState:state];
    return [receipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString];
}
複製程式碼

3.總結

當我們利用這個分類為按鈕設定圖片時

  • 首先會根據要設定的控制元件狀態和圖片連結判斷是否是重複下載,如果是重複下載就不繼續向下進行了。
  • 接著會取消在設定的控制元件狀態下正在進行的下載操作(如果有的話),也就是說只有最新的這次圖片設定才有效。
  • 然後獲取到圖片下載器的圖片快取物件,從圖片快取物件中查詢是否有快取,如果有快取的話會直接取出新增到按鈕上,但是如果使用者設定了成功回撥block,就不會將圖片新增到按鈕上,而是會呼叫blokc,將結果傳遞給使用者,讓使用者來處理。
  • 如果沒有快取,就會先判斷有沒有佔點陣圖,如果有佔點陣圖的話,就先給按鈕設定佔點陣圖。
  • 接下來將UUID作為識別符號開啟圖片下載任務,並將任務封裝物件儲存在屬性中,屬性的命名是根據圖片要設定的控制元件狀態相關聯的。
  • 下載任務執行完成後,無論成功還是失敗,都會先根據設定的控制元件狀態獲取到對應的下載任務封裝物件,對比下載任務封裝物件的識別符號和UUID判斷是否是同一任務的回撥,只用是同一任務的回撥才繼續執行
  • 在下載成功的情況下,如果使用者設定了成功回撥block,就呼叫block,將結果傳遞給使用者,讓使用者來處理,如果沒有設定,就直接將下載好的圖片新增到按鈕的指定狀態上,並將指定狀態所對應的儲存下載任務封裝物件的屬性置nil。
  • 當下載失敗時,如果使用者設定了失敗回撥block,就呼叫block,將結果傳遞給使用者,讓使用者來處理,如果沒有設定,就將指定狀態所對應的儲存下載任務封裝物件的屬性置nil。

原始碼閱讀系列:AFNetworking

原始碼閱讀:AFNetworking(一)——從使用入手

原始碼閱讀:AFNetworking(二)——AFURLRequestSerialization

原始碼閱讀:AFNetworking(三)——AFURLResponseSerialization

原始碼閱讀:AFNetworking(四)——AFSecurityPolicy

原始碼閱讀:AFNetworking(五)——AFNetworkReachabilityManager

原始碼閱讀:AFNetworking(六)——AFURLSessionManager

原始碼閱讀:AFNetworking(七)——AFHTTPSessionManager

原始碼閱讀:AFNetworking(八)——AFAutoPurgingImageCache

原始碼閱讀:AFNetworking(九)——AFImageDownloader

原始碼閱讀:AFNetworking(十)——AFNetworkActivityIndicatorManager

原始碼閱讀:AFNetworking(十一)——UIActivityIndicatorView+AFNetworking

原始碼閱讀:AFNetworking(十二)——UIButton+AFNetworking

原始碼閱讀:AFNetworking(十三)——UIImageView+AFNetworking

原始碼閱讀:AFNetworking(十四)——UIProgressView+AFNetworking

原始碼閱讀:AFNetworking(十五)——UIRefreshControl+AFNetworking

原始碼閱讀:AFNetworking(十六)——UIWebView+AFNetworking

相關文章