[iOS][OC] 利用 method-swizzling 或继承多态对SDWebImage的返回请求头进行过滤

背景


SDWebImage 是著名的 iOS 的 OC 第三方库,可以很好地辅助开发者做好网络图片的请求加载和缓存工作,而在一些后台逻辑场景下,存在着局限性,需要开发者去扩展。比如,通过URL请求一张图片,此图片不存在或者图片无权限时,后台接口不是返回错误 statusCode,而是会返回一个提示出错的图片,并在返回 response 的请求头中协议一个错误 errorCode 字段,SDWebImage 成功接收到一张图片后即展示并缓存图片,一段时间内都无法再次请求这个 URL 图片,因而出现这类出错的图片长期无法刷新的问题。
针对这类问题,分两种情况,

  • 如果这类情况只针对少数图片,在设置图片时设置缓存策略SDWebImageOptions为“仅适用于内存缓存”SDWebImageCacheMemoryOnly,如此不存在硬盘缓存从而会发起新的请求
  • 如果这种情况十分普遍,则合适的方案是对 SDWebImage 的每一次请求的回调进行请求头的过滤,当存在错误 errorCode 字段时不再执行成功的回调而走向失败,显示 app 端的占位图片 placeholderImage ,以避免在硬盘中缓存了错误图片。

请求头过滤的实现

SDWebImage中,网络请求的发起和回调的操作处理由 SDWebImageDownloader 执行,最后转发给 ````SDWebImageDownloaderOperation这个类进行操作处理,此类继承自NSOperation,其遵循了NSURLSessionTaskDelegateNSURLSessionDataDelegate```两个协议,封装了请求的发起和回调逻辑,通过将这些操作加入 NSOperationQueue 后进行管理和执行。
查看 SDWebImageDownloaderOperation 的源码可以发现,执行请求成功/失败的判断是在 URLSessionDelegate 的一个协议方法中执行的,代码如下:

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

从中可以看到当请求回调的 response 中 statusCoe 在一定范围内( s < 400 && s != 304)会走向业务成功的处理(保存图片数据和reponse),否则走向失败的处理(取消请求任务 task->cancel),因此应该在执行 response 的判断之前,进行拦截。

顺便说一下,这里可以看到 NSURLSession 的回调,采用的是 delegate+block 的```回调后再调用`` 的设计思路。可以参考文章了解: [iOS] [OC] 关于block回调、高阶函数“回调再调用”及项目实践

在不修改 SDWebImage 源代码的情况下,有两种可行的方案,分别是多态和runtime交换方法实现 method-swizzling,分别实现如下:

多态方案

从 SDWebImageDownloader 的 API 中可以找到用设置 SDWebImageDownloaderOperation 生成类的方法 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;

也就是说,可以通过设置自定义的 SDWebImageDownloaderOperation 子类来自定义请求逻辑,再结合继承后多态的特性,在子类中 复写协议方法对 NSURLSessionTaskDelegate 方法进行重写判断完请求头后再调用 super 继续父类原有逻辑。代码如下:

@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; // 写任意一个 SDWebImageDownloaderOperation 会判断为错误的 code 即可 (大于400)
    // 置换一个新的实例
    response = [[NSHTTPURLResponse alloc] initWithURL:response.URL
                                                   statusCode:statusCode
                                                   HTTPVersion:@"HTTP/1.1" // 这里写死了没有影响
                                          headerFields:headers];
}


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

}

@end

其核心是当 errorCode 判断无效时,返回一个伪造的异常 response 使得原有逻辑走向错误处理 else,在 程序启动时配置好下载图片的子类如下:

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

方案二:利用 method-swizzling,在调用 SDWebImageDownloader 方法之前,先 执行请求头的判断,再执行原方法。过程是创建 SDWebImageDownloaderOperation 分类 category,交换内部代理方法的实现,尝试实现如下:

#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; // 写任意一个 SDWebImageDownloaderOperation 会判断为错误的 code 即可 (大于400)
    // 置换一个新的实例
    response = [[NSHTTPURLResponse alloc] initWithURL:response.URL
         statusCode:statusCode
       HTTPVersion:@"HTTP/1.1" // 这里只能写死了,应该没有影响
     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

小结

两种方案的效果是一致的,在 SDWebImage 有 API 支持的情况下,建议优先使用既有 API 的方案一, 而 swizzle 可以在其他类似场景下使用。

加我微信沟通。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345