[iOS][OC] 利用 method-swizzling 或繼承多態(tài)對(duì)SDWebImage的返回請(qǐng)求頭進(jìn)行過(guò)濾

背景


SDWebImage 是著名的 iOS 的 OC 第三方庫(kù),可以很好地輔助開(kāi)發(fā)者做好網(wǎng)絡(luò)圖片的請(qǐng)求加載和緩存工作,而在一些后臺(tái)邏輯場(chǎng)景下,存在著局限性,需要開(kāi)發(fā)者去擴(kuò)展。比如,通過(guò)URL請(qǐng)求一張圖片,此圖片不存在或者圖片無(wú)權(quán)限時(shí),后臺(tái)接口不是返回錯(cuò)誤 statusCode,而是會(huì)返回一個(gè)提示出錯(cuò)的圖片,并在返回 response 的請(qǐng)求頭中協(xié)議一個(gè)錯(cuò)誤 errorCode 字段,SDWebImage 成功接收到一張圖片后即展示并緩存圖片,一段時(shí)間內(nèi)都無(wú)法再次請(qǐng)求這個(gè) URL 圖片,因而出現(xiàn)這類(lèi)出錯(cuò)的圖片長(zhǎng)期無(wú)法刷新的問(wèn)題。
針對(duì)這類(lèi)問(wèn)題,分兩種情況,

  • 如果這類(lèi)情況只針對(duì)少數(shù)圖片,在設(shè)置圖片時(shí)設(shè)置緩存策略SDWebImageOptions為“僅適用于內(nèi)存緩存”SDWebImageCacheMemoryOnly,如此不存在硬盤(pán)緩存從而會(huì)發(fā)起新的請(qǐng)求
  • 如果這種情況十分普遍,則合適的方案是對(duì) SDWebImage 的每一次請(qǐng)求的回調(diào)進(jìn)行請(qǐng)求頭的過(guò)濾,當(dāng)存在錯(cuò)誤 errorCode 字段時(shí)不再執(zhí)行成功的回調(diào)而走向失敗,顯示 app 端的占位圖片 placeholderImage ,以避免在硬盤(pán)中緩存了錯(cuò)誤圖片。

請(qǐng)求頭過(guò)濾的實(shí)現(xiàn)

SDWebImage中,網(wǎng)絡(luò)請(qǐng)求的發(fā)起和回調(diào)的操作處理由 SDWebImageDownloader 執(zhí)行,最后轉(zhuǎn)發(fā)給 ````SDWebImageDownloaderOperation這個(gè)類(lèi)進(jìn)行操作處理,此類(lèi)繼承自NSOperation,其遵循了NSURLSessionTaskDelegateNSURLSessionDataDelegate```兩個(gè)協(xié)議,封裝了請(qǐng)求的發(fā)起和回調(diào)邏輯,通過(guò)將這些操作加入 NSOperationQueue 后進(jìn)行管理和執(zhí)行。
查看 SDWebImageDownloaderOperation 的源碼可以發(fā)現(xiàn),執(zhí)行請(qǐng)求成功/失敗的判斷是在 URLSessionDelegate 的一個(gè)協(xié)議方法中執(zhí)行的,代碼如下:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

//'304 Not Modified' is an exceptional one
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
    NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
    self.expectedSize = expected;
    if (self.progressBlock) {
       self.progressBlock(0, expected);
    }
    
    self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
    self.response = response;
    dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
    });
}
else {
    NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
    
    //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
    //In case of 304 we need just cancel the operation and return cached image from the cache.
    if (code == 304) {
       [self cancelInternal];
    } else {
       [self.dataTask cancel];
    }
    dispatch_async(dispatch_get_main_queue(), ^{
       [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
    });
    
    if (self.completedBlock) {
       self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
    }
       [self done];
    }

    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

從中可以看到當(dāng)請(qǐng)求回調(diào)的 response 中 statusCoe 在一定范圍內(nèi)( s < 400 && s != 304)會(huì)走向業(yè)務(wù)成功的處理(保存圖片數(shù)據(jù)和reponse),否則走向失敗的處理(取消請(qǐng)求任務(wù) task->cancel),因此應(yīng)該在執(zhí)行 response 的判斷之前,進(jìn)行攔截。

順便說(shuō)一下,這里可以看到 NSURLSession 的回調(diào),采用的是 delegate+block 的```回調(diào)后再調(diào)用`` 的設(shè)計(jì)思路。可以參考文章了解: [iOS] [OC] 關(guān)于block回調(diào)、高階函數(shù)“回調(diào)再調(diào)用”及項(xiàng)目實(shí)踐

在不修改 SDWebImage 源代碼的情況下,有兩種可行的方案,分別是多態(tài)和runtime交換方法實(shí)現(xiàn) method-swizzling,分別實(shí)現(xiàn)如下:

多態(tài)方案

從 SDWebImageDownloader 的 API 中可以找到用設(shè)置 SDWebImageDownloaderOperation 生成類(lèi)的方法 setOperationClass::


* Sets a subclass of `SDWebImageDownloaderOperation` as the default
* `NSOperation` to be used each time SDWebImage constructs a request
* operation to download an image.
*
* @param operationClass The subclass of `SDWebImageDownloaderOperation` to set
*        as default. Passing `nil` will revert to `SDWebImageDownloaderOperation`.

- (void)setOperationClass:(Class)operationClass;

也就是說(shuō),可以通過(guò)設(shè)置自定義的 SDWebImageDownloaderOperation 子類(lèi)來(lái)自定義請(qǐng)求邏輯,再結(jié)合繼承后多態(tài)的特性,在子類(lèi)中 復(fù)寫(xiě)協(xié)議方法對(duì) NSURLSessionTaskDelegate 方法進(jìn)行重寫(xiě)判斷完請(qǐng)求頭后再調(diào)用 super 繼續(xù)父類(lèi)原有邏輯。代碼如下:

@interface WBWebImageDownloaderOperation : SDWebImageDownloaderOperation

@end

@implementation WBWebImageDownloaderOperation

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSHTTPURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSInteger statusCode = response.statusCode;
    NSDictionary *headers = response.allHeaderFields;

    if ([headers[@"ErrorCode"] integerValue] == 100) {
    // 異常
    statusCode = 444; // 寫(xiě)任意一個(gè) SDWebImageDownloaderOperation 會(huì)判斷為錯(cuò)誤的 code 即可 (大于400)
    // 置換一個(gè)新的實(shí)例
    response = [[NSHTTPURLResponse alloc] initWithURL:response.URL
                                                   statusCode:statusCode
                                                   HTTPVersion:@"HTTP/1.1" // 這里寫(xiě)死了沒(méi)有影響
                                          headerFields:headers];
}


[super URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];

}

@end

其核心是當(dāng) errorCode 判斷無(wú)效時(shí),返回一個(gè)偽造的異常 response 使得原有邏輯走向錯(cuò)誤處理 else,在 程序啟動(dòng)時(shí)配置好下載圖片的子類(lèi)如下:

// application:didFinishLaunchWithOptions:
[[SDWebImageDownloader sharedDownloader] setOperationClass:[WBWebImageDownloaderOperation class]];

方案二:利用 method-swizzling,在調(diào)用 SDWebImageDownloader 方法之前,先 執(zhí)行請(qǐng)求頭的判斷,再執(zhí)行原方法。過(guò)程是創(chuàng)建 SDWebImageDownloaderOperation 分類(lèi) category,交換內(nèi)部代理方法的實(shí)現(xiàn),嘗試實(shí)現(xiàn)如下:

#import "SDWebImageDownloaderOperation+WBSwizzle.h"
#import <objc/runtime.h>

@implementation SDWebImageDownloaderOperation (WBSwizzle)

+ (void)load {
  [self swizzleInstanceMethod:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)
                  withMethod:@selector(wb_URLSession:dataTask:didReceiveResponse:completionHandler:)
                      class:self];
}

- (void)wb_URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSHTTPURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    NSInteger statusCode = response.statusCode;
    NSDictionary *headers = response.allHeaderFields;
    if ([headers[@"ErrorCode"] integerValue] == 100) {
    // 異常
    statusCode = 444; // 寫(xiě)任意一個(gè) SDWebImageDownloaderOperation 會(huì)判斷為錯(cuò)誤的 code 即可 (大于400)
    // 置換一個(gè)新的實(shí)例
    response = [[NSHTTPURLResponse alloc] initWithURL:response.URL
         statusCode:statusCode
       HTTPVersion:@"HTTP/1.1" // 這里只能寫(xiě)死了,應(yīng)該沒(méi)有影響
     headerFields:headers];
}

[self wb_URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}

+ (void)swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector class:(Class)cls
{
// if current class not exist selector, then get super
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
// add selector if not exist, implement append with method
if (class_addMethod(cls,
                   origSelector,
                   method_getImplementation(swizzledMethod),
                   method_getTypeEncoding(swizzledMethod)) ) {
   // replace class instance method, added if selector not exist
   // for class cluster , it always add new selector here
   class_replaceMethod(cls,
                       newSelector,
                       method_getImplementation(originalMethod),
                       method_getTypeEncoding(originalMethod));
   
} else {
   // swizzleMethod maybe belong to super
   class_replaceMethod(cls,
                       newSelector,
                       class_replaceMethod(cls,
                                           origSelector,
                                           method_getImplementation(swizzledMethod),
                                           method_getTypeEncoding(swizzledMethod)),
                       method_getTypeEncoding(originalMethod));
}
}

@end

小結(jié)

兩種方案的效果是一致的,在 SDWebImage 有 API 支持的情況下,建議優(yōu)先使用既有 API 的方案一, 而 swizzle 可以在其他類(lèi)似場(chǎng)景下使用。

加我微信溝通。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容