问题场景
最近有这么一个场景,客户端使用 SDWebImage 请求一张用户头像图片,服务端若是发现客户端请求的用户头像图片 404 错误 (有可能该图片被删除了,反正就是不存在了) 那么服务端给 response 的 header 添加一个字段 ErrorCode,值为 100 ,并返回默认错误图片。这个时候客户端会显示服务端返回的默认错误图并缓存该图片。当服务端更新了原本 404 的用户头像图片之后并不会重新更新用户头像链接。不知道你有没有发现问题了 ? 客户端使用了 SDWebImage 缓存了图片之后,当需要再次使用该链接的图片的时候,客户端使用的 SDWebImage 并不会再次发起请求,而是使用缓存图片。这样的话,客户端会一直使用错误的用户头像。
解决方案
基于上面的使用场景,我需要让 SDWebImage 在下载图片的时候对 response 的 header 进行判断,在
header 中,若是存在一个 ErrorCode 的key并且 value 的值为 100。那么我就不缓存这张图片。这样客户端就不会缓存到错误的用户头像图片了。
实现思路
在对一个开源项目的做简单修改之前,我们需要对这个开源项目有个大概的了解,最好的了解渠道就是GitHub 主页,SDWebImage 的 GitHub 主页上有 SDWebImage 的架构图和流程图。
我们先看看流程图,流程图描述了 SDWebImage 的主要工作流程。
在流程图中,可以看到和图片下载请求相关的类是 SDWebImageDownloader 。
我们看完了流程图,找到了 SDWebImageDownloader 这个负责图片下载的类,接下来一起看看架构图,看能否得到更多关于 SDWebImageDownloader 这个类的信息。
从架构图中可以看出,下载相关的操作确实是在 SDWebImageDownloader 里面,而且
SDWebImageDownloader 依赖 SDWebImageDownloaderOperation。初步判断,SDWebImageDownloaderOperation 是真正执行图片下载请求的地方。
接下看就是看代码的时间了,先找到 SDWebImageDownloaderOperation 类所在位置
下一步,查看 SDWebImageDownloaderOperation.m 里面的下载相关的方法。从图中可以看到了一些 NSURLSessionDataDelegate 的方法,这个很明显是网络请求的代码了。
下一步,查看 SDWebImageDownloaderOperation 类的 NSURLSessionDataDelegate 实现方法 ,这个方法是用来接收网络请求响应的,所以我们在这里读取 response 的 header,判断服务端是否返回错误图片。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
// 省略代码
}
把这个方法的代码通读一遍,想到了具体的实现方案,读取 response 的 header,若是存在一个 ErrorCode 的key并且 value 的值为 100,那么走原实现方法的 statusCode 大于 400 的判断分支。
//收到响应
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
//'304 Not Modified' is an exceptional one
// (没有statusCode) 或者 (statusCode小于400 并且 statusCode 不等于304)
// 若是请求响应成功,statusCode是200,那么会进入这个代码分支
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;
//下载过程回调,收到数据量为0
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
//根据期望收到的数据量创建NSMutableData,该NSMutableData用户保存收到的数据量
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) {
//code 304 取消下载 直接返回缓存中的内容
[self cancelInternal];
} else {
//数据请求任务取消
[self.dataTask cancel];
}
//发送停止下载的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// 错误处理回调
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
[self done];
}
// 调用completionHandler回调
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
修改后的代码如下,修改的位置有 3 个
- 修改的地方 1,读取 header,判断是否是错误图片
- 修改的地方 2,增加一个 isErrorImage 的判断条件,控制代码执行条件
- 修改的地方 3,增加一个 isErrorImage 的判断条件,控制代码执行条件
修改完成之后,执行代码,验证成功。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// 修改的地方 1
//是否是错误图片的标志位,用于控制代码 if 分支语句的执行
BOOL isErrorImage = NO;
//取 header 判断 ErrorCode 是否为 100,若为 100 则判定为错误图片
NSDictionary *allHeaderFields =[(NSHTTPURLResponse *)response allHeaderFields];
if ([allHeaderFields objectForKey:@"ErrorCode"]) {
NSNumber *code = [allHeaderFields objectForKey:@"ErrorCode"];
if ([code integerValue] == 100) {
isErrorImage = YES;
}
}
// 修改的地方 2
if ( (!isErrorImage) && (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304))) {
// 省略代码
}
else {
NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
// 修改的地方 3
if (code == 304 && (!isErrorImage)) {
//code 304 取消下载 直接返回缓存中的内容
[self cancelInternal];
} else {
//数据请求任务取消
[self.dataTask cancel];
}
// 省略代码
}
// 省略代码
总结
对开源项目的修改通常都需要对整个项目有大致的理解,然后再根据自己的需求去寻找需要改动的点,最后进行验证测试。在这个过程中,对开源项目的理解或者了解越深,改动起来就越快!