背景
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,其遵循了NSURLSessionTaskDelegate和NSURLSessionDataDelegate```兩個(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)景下使用。
加我微信溝通。
