NSURLProtocol无法截获NSURLSession解决方案

前言

在做知乎日报的项目时,在其设置界面中有一个选项,当开启时,可以在移动网络状况下不下载图片。这个需求的解决方案有两种,一种是在每一个使用网络图片的地方,在下载前进行判断,如果当前网络为移动蜂窝网络,且开启了在移动网络状况下不下载图片的选项时,放弃图片的下载;还有一种方案就是直接在NSURLProtocol中进行截获,在+ (BOOL)canInitWithRequest:(NSURLRequest *)request中进行判断,当request.URL请求为图片,并开启不下载图片选项,且当前为移动网络状况下,截获该请求。在知乎日报项目中,我使用的第二种,既子类NSURLProtocol来实现请求拦截。

NSURLProtocol实现图片下载拦截

一开始,觉得思路很简单,直接创建PictureBlockURLProtocol类,其是NSURLProtocol的子类,在Implement实现中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//获取当前网络状态
+ (BOOL)isBlockPictureDownload{
Reachability *reachability = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).reachability;

NetworkStatus netStatus = [reachability currentReachabilityStatus];

BOOL isBlock = netStatus == ReachableViaWWAN?YES : NO;

return isBlock;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([NSURLProtocol propertyForKey:HybridResourceProtocolKey inRequest:request]) {
return NO;
}

//满足条件时直接截获请求
if ([[UserConfig sharedInstance] isBlockPicture] && ([request.URL.pathExtension caseInsensitiveCompare:@"jpg"] == NSOrderedSame || [request.URL.pathExtension caseInsensitiveCompare:@"png"] == NSOrderedSame) && [self isBlockPictureDownload]) {
return YES;
}
return NO;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}

//没有实现内容,request会以请求超时的方式结束
- (void)startLoading{}

- (void)stopLoading{}

定义完PictureBlockURLProtocol类后,在AppDelegate类的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中调用[NSURLProtocol registerClass:[PictureBlockURLProtocol class]]方法进行注册,运行一下,看看效果,发现,当使用AFNetwork、SDWebImageCache等第三方库的图片请求并没有被截获到,而UIWebView可以,这就奇怪了,不过一想,UIWebView使用的是NSURLConnection,而AFNetWorkSDWebCache等已经废弃使用NSURLConnection,转而使用NSURLSession,难道是由于这个问题造成的?

在查看NSURLSession相关类的文档时(如下图),从文档可以看到,如果需要支持自定义的NSURLProtocol,需要将自定义的NSURLProtocol子类赋给NSURLSessionConfigurationprotocolClasses属性。所以,如果需要NSURLProtocol来截获NSURLSession发出的请求,需要每一个NSURLSession在创建时配置的NSURLSessionConfiguration类的protocolClasses属性附上自定义的NSURLProtocol

根据上图的文档,注意到,对于后台Sessions,是不支持自定义的NSURLProtocol的。

通过查看AFNetwork、SDWebImageCache的源代码,其创建的Session时关联的NSURLSessionConfigurationdefaultSessionConfiguration。这样,解决方案就有两种,一种是直接改AFNetwork、SDWebImageCache等库的源代码,在其创建的NSURLSessionConfiguration实例中将自定义的NSURLProtocol赋给其protocolClasses属性;另一种方案,使用Method Swizzling方法,由于AFNetwork、SDWebImageCache在创建时使用的是[NSURLSessionConfiguration defaultSessionConfiguration],所以可以替换defaultSessionConfiguration实现。首先,创建NSURLSessionConfiguration的类别,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (NSURLSessionConfiguration *)zw_defaultSessionConfiguration{
NSURLSessionConfiguration *configuration = [self zw_defaultSessionConfiguration];
NSArray *protocolClasses = @[[PictureBlockURLProtocol class]];
configuration.protocolClasses = protocolClasses;

return configuration;
}

+ (void)load{
Method systemMethod = class_getClassMethod([NSURLSessionConfiguration class], @selector(defaultSessionConfiguration));
Method zwMethod = class_getClassMethod([self class], @selector(zw_defaultSessionConfiguration));
method_exchangeImplementations(systemMethod, zwMethod);

[NSURLProtocol registerClass:[PictureBlockURLProtocol class]];
}

如上代码所示,在创建的每一个NSURLSessionConfiguration实例中,添加自定义的NSURLProtocol类到其protocolClasses属性,运行后,发现问题解决了,可以截获NSURLSession发出的请求了。

Done!!!!