IOS进阶:(网络篇)NSURLProtocol网络拦截
原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
Demo在我的Github上,欢迎下载。
NetworkAdvancedDemo
目录
- NSURLProtocol介绍
- 含义
- 具体流程
- 使用场景
- Demo实战
- CustomURLProtocol
- 加载WebView和WKWebView
- 加载NSURLSession
- 加载NSURLConnection
- 加载AFNetworking
- 拓展:使用runtime来实现加载AFNetworking
NSURLProtocol介绍

含义:
NSURLProtocol
是苹果为我们提供的 URL Loading System
的一部分,能够让你去重新定义苹果的URL Loading System
的行为。
用一句话解释NSURLProtocol
:就是一个苹果允许的中间人攻击。
NSURLProtocol
可以劫持系统所有基于CFsocket
的网络请求。不管你是通过UIWebView
, NSURLConnection
或者第三方库 (AFNetworking
, Alamofire
等),他们都是基于NSURLConnection
或者NSURLSession
实现的,因此你可以通过NSURLProtocol
做自定义的操作。
⚠️:WKWebView
基于Webkit
,并不走底层的CFsocket
,所以NSURLProtocol
拦截不了WKWebView
中的请求。
具体流程:
URL Loading System
里有许多类用于处理URL
请求,比如NSURL
,NSURLRequest
,NSURLConnection
和NSURLSession
等,当URL Loading System
使用NSURLRequest
去获取资源的时候,它会创建一个NSURLProtocol
子类的实例,NSURLProtocol
看起来像是一个协议,但其实这是一个类,你不能直接实例化一个NSURLProtocol
,而是需要写一个继承自 NSURLProtocol
的子类,并通过- registerClass:
方法注册我们的协议类,然后 URL
加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。
简单归纳下,使用NSURLProtocol
的主要可以分为5个步骤:注册—>拦截—>转发—>回调—>结束
即:注册NSURLProtocol
子类 -> 使用NSURLProtocol
子类拦截请求 -> 使用NSURLSession
重新发起请求 -> 将NSURLSession
请求的响应内容返回 -> 结束
使用场景:
举个例子:因为DNS
发生域名劫持,所以需要手动将URL
请求的域名重定向到指定的IP
地址,但是由于请求可能是通过NSURLConnection
,NSURLSession
或者AFNetworking
等方式,因此要想统一进行处理,可以采用NSURLProtocol
。
- 重定向网络请求(可以解决
DNS
域名劫持问题) - 忽略网络请求,使用本地缓存
- 自定义网络请求的返回结果
Response
- 拦截图片加载请求,转为从本地文件加载
- 一些全局的网络请求设置
- 快速进行测试环境的切换
- 过滤掉一些非法请求
- 网络的缓存处理(如网络图片缓存)
- 可以拦截
UIWebView
,基于系统的NSURLConnection
或者NSURLSession
进行封装的网络请求。目前WKWebView
无法被NSURLProtocol
拦截。 - 当有多个自定义
NSURLProtocol
注册到系统中的话,会按照他们注册的反向顺序依次调用URL
加载流程。当其中有一个NSURLProtocol
拦截到请求的话,后续的NSURLProtocol
就无法拦截到该请求。
Demo实战
CustomURLProtocol
a、子类化
由于 NSURLProtocol
是一个抽象类,所以使用的时候必须先定义一个它的子类,这里我们新建CustomURLProtocol
继承自NSURLProtocol
@interface CustomURLProtocol : NSURLProtocol
@end
b、注册
对于基于NSURLConnection
或者使用[NSURLSession sharedSession]
初始化对象创建的网络请求,调用registerClass
方法即可
- (void)viewDidLoad
{
[super viewDidLoad];
// 注册NSURLProtocol的子类
[NSURLProtocol registerClass:[CustomURLProtocol class]];
}
一经注册之后,所有交给URL Loading system
的网络请求都会被拦截,所以当不需要拦截的时候,要进行注销
- (void)dealloc
{
// 一经注册之后,所有交给URL Loading system的网络请求都会被拦截,所以当不需要拦截的时候,要进行注销
[NSURLProtocol unregisterClass:[CustomURLProtocol class]];
}
c、抽象对象必须实现的拦截方法
canInitWithRequest:所有注册此Protocol
的请求都会经过这个方法的判断,该方法会拿到request
的对象,我们可以通过该方法的返回值来筛选request
是否需要被NSURLProtocol
做拦截处理。

此处尝试拦截 http://www.baidu.com/ 即百度搜索首页其中的标题栏的Logo图片,首先需要在打印出来的absoluteString
找到我们想要的Logo
图片的URL
,接着通过判断是否相等进行拦截,返回YES
即进入拦截流程。
// 通过该方法的返回值来筛选request是否需要被NSURLProtocol做拦截处理
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 获取所有的absoluteString
NSString *absoluteString = [[request URL] absoluteString];
NSLog(@"absoluteString--%@",absoluteString);
// 拦截百度标题栏的logo图片,返回YES进行拦截,目的是替换为自己的海贼王图片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
return YES;
}
// 默认返回NO,不进行拦截
return NO;
}
canonicalRequestForRequest:可选方法,对需要拦截的请求进行自定义的处理,这个方法用来统一处理请求request
对象的,可以修改头信息,或者重定向。没有特殊需要,则直接return request
。通常我们的做法是直接return request
,在后面的startLoading
方法中进行拦截处理。还有一点需要注意的是,如果要在这里做重定向以及头信息的时候注意检查是否已经添加,因为这个方法可能被调用多次。
// 可选方法,对需要拦截的请求进行自定的处理,没有特殊需要,则直接return request
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
requestIsCacheEquivalent:用来判断两个request
请求是否相同,这个方法基本不常用。如果相同,则可以使用缓存数据。通常只需要调用父类的实现即可,默认为YES
。
// 用来判断两个request请求是否相同,这个方法基本不常用,通常只需要调用父类的实现即可
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
initWithRequest:在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去,就可以初始化一个NSURLProtocol
对象了。该方法会创建一个NSURLProtocol
实例,在这里直接调用super
的指定构造器方法,实例化一个对象。
// 该方法会创建一个NSURLProtocol实例,在这里直接调用super的指定构造器方法,将网络请求重新发送出去
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}
转发的核心方法startLoading
开始请求的方法,在该方法中,把当前请求的request
拦截下来以后,可以在这里修改请求信息,重定向网络,DNS
解析,使用自定义的缓存等。
在这里需要我们手动的把请求发出去,可以使用原生的NSURLConnection
、NSURLSessionDataTask
,也可以使用的第三方网络库AFNetworking
。对于NSURLConnection
来说,就是创建一个NSURLConnection
,对于NSURLSession
,就是发起一个NSURLSessionDataTask
,同时设置NSURLSessionDataDelegate
协议,接收Server
端的响应。
⚠️
- 一般下载前需要设置该请求正在进行下载,防止多次下载的情况发生。
- 这个方法之后,会回调
<NSURLProtocolClient>
协议中的方法。
此处我们想要拦截百度标题栏的logo
图片,再将其替换为自己本地的海贼王图片,所以首先我们需要一个获取本地图片的方法。
// 取出本地图片
- (NSData *)getImageData
{
NSString *fileName = [[NSBundle mainBundle] pathForResource:@"haizeiwang.jpg" ofType:@""];
return [NSData dataWithContentsOfFile:fileName];
}
接着调用client
的didLoadData
加载数据方法。
// 转发的核心方法,在这里需要我们手动的把请求发出去
// 可以使用原生的NSURLConnection、NSURLSessionDataTask,也可以使用的第三方网络库AFNetworking
- (void)startLoading
{
// 获取所有的absoluteString
NSString *absoluteString = [[self.request URL] absoluteString];
// 拦截百度标题栏的logo图片,替换为自己本地的海贼王图片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
// 取出本地图片
NSData *data = [self getImageData];
// 接着调用client的didLoadData加载数据方法
[self.client URLProtocol:self didLoadData:data];
}
}
stopLoading:请求被停止,结束网络请求的操作。当NSURLProtocolClient
的协议方法都回调完毕后,就会开始执行这个方法了。
WebView和WKWebView的加载
a、引入#import <WebKit/WebKit.h>
框架,再声明webView
和wk
变量
@interface NSURLProtocolViewController ()
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) WKWebView *wk;
@end
b、实现webViewButton
的回调事件loadWebView
,首先需要移除之前的UIWebView
,WKWebView
,并进行网络请求,这里为百度首页。
// 加载WebView
- (void)loadWebView
{
// 移除旧的
[self.webView removeFromSuperview];
[self.wk removeFromSuperview];
self.webView = nil;
self.wk = nil;
// 创建新的UIWebView
self.webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height - 300, self.view.bounds.size.width, 300)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[self.webView loadRequest:request];
[self.view addSubview:self.webView];
}

拦截成功后替换为了我们自己的海贼王图片

c、刚才只是替换了一张图片,如果我想一次性替换所有图片为我的海贼王呢?只需要修改下
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 获取所有的absoluteString
NSString *absoluteString = [[request URL] absoluteString];
NSLog(@"absoluteString--%@",absoluteString);
/* 拦截百度标题栏的logo图片,返回YES进行拦截,目的是替换为自己的海贼王图片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
return YES;
}
*/
// 直接hook所有图片:比较URL的后缀是否属于图片,是则自定义忽略掉
NSString* extension = request.URL.pathExtension;
NSArray *array = @[@"png", @"jpeg", @"gif", @"jpg"];
if([array containsObject:extension]){
return YES;
}
// 默认返回NO,不进行拦截
return NO;
}
- (void)startLoading
{
// 获取所有的absoluteString
NSString *absoluteString = [[self.request URL] absoluteString];
/* 拦截百度标题栏的logo图片,替换为自己本地的海贼王图片
if ([absoluteString isEqualToString:@"https://www.baidu.com/img/flexible/logo/plus_logo_web.png"])
{
// 取出本地图片
NSData *data = [self getImageData];
// 接着调用client的didLoadData加载数据方法
[self.client URLProtocol:self didLoadData:data];
}
*/
// 只要是图片,全部替换为海贼王
NSString* extension = self.request.URL.pathExtension;
NSArray *array = @[@"png", @"jpeg", @"gif", @"jpg"];
if([array containsObject:extension])
{
// 取出本地图片
NSData *data = [self getImageData];
// 接着调用client的didLoadData加载数据方法
[self.client URLProtocol:self didLoadData:data];
}
}

图片加载的一般都是广告,实体数据有一层model
包装,所以只会去除掉广告而不会打扰到实体数据。
d、接着实现wkWebViewButton
的回调事件loadWKWebView
,过程同上
// 加载WKWebView
- (void)loadWKWebView
{
// 移除旧的
[self.webView removeFromSuperview];
[self.wk removeFromSuperview];
self.webView = nil;
self.wk = nil;
// 创建新的WKWebView
self.wk = [[WKWebView alloc] initWithFrame:CGRectMake(0, 300, self.view.bounds.size.width, 600)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[self.wk loadRequest:request];
[self.view addSubview:self.wk];
}

问题
WKWebView
打印出absoluteString
,无法找到百度Logo
图片,所以不能进行拦截替换图片。原因是WKWebView
在独立于app
进程之外的进程(webkit
)中执行网络请求,请求数据不经过主进程(URL Loading System
),因此,在WKWebView
上直接使用 NSURLProtocol
无法拦截请求。
原理
其实WKWebview
在一开始时候是会调用到NSURLProtocol
中的入口方法canInitWithRequest
的,但是就没有然后了,也就是说WKWebview
是和NSURLProtocol
有一定关联,只是在NSURLProtocol
的入口处返回NO
所以导致NSURLProtocol
不接管WKWebview
的请求。
返回YES
的规则便是你所请求的URL
的Scheme
要和它内部配置的CustomScheme
相同。不过这里有一个疑问,苹果在使用webkit
时候为什么会把http/https
这样大众化的scheme
过滤掉,看来他是不建议开发者来使用NSURLProtocol
。
解决方案
只要我们在注册完我们自己的CustomProtocol
之后在调用该方法应该就可以了,因为registerSchemeForCustomProtocol
是WKBrowsingContextController
的类方法,所以只能用WKBrowsingContextController
去调用,但是在webkit
的头文件发现WKBrowsingContextController
并没有开放出来,所以我们采用NSClassFromString
和NSSelectorFromString
方法来拿到类和对应的方法。
// 加载WKWebView
- (void)loadWKWebView
{
// 移除旧的
[self.webView removeFromSuperview];
[self.wk removeFromSuperview];
self.webView = nil;
self.wk = nil;
// 创建新的WKWebView
self.wk = [[WKWebView alloc] initWithFrame:CGRectMake(0, 300, self.view.bounds.size.width, 600)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[self.wk loadRequest:request];
[self.view addSubview:self.wk];
//注册scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
// cls 是否包含 sel方法
if ([cls respondsToSelector:sel]) {
// 通过http和https的请求,同理可通过其他的Scheme 但是要满足ULR Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}
}

注意点
-
关于私有API:因为
WKBrowsingContextController
和registerSchemeForCustomProtocol
应该是私有的所以使用时候需要对字符串做下处理,用加密的方式或者其他就可以了,实测可以过审核的。 -
关于post请求:大家会发现拦截不了
post
请求(拦截到的post
请求body
体为空),这个其实和WKWebview
没有关系,这个是苹果为了提高效率加快流畅度所以在NSURLProtocol
拦截之后索性就不复制body
体内的东西,因为body
的大小没有限制,开发者可能会把很大的数据放进去那就不好办了。我们可以采取httpbodystream
的方式拿到body
。
// 处理POST请求相关POST 用HTTPBodyStream来处理BODY体
- (NSMutableURLRequest *)handlePostRequestBodyWithRequest:(NSMutableURLRequest *)request {
NSMutableURLRequest * req = [request mutableCopy];
if ([request.HTTPMethod isEqualToString:@"POST"]) {
if (!request.HTTPBody) {
uint8_t d[1024] = {0};
NSInputStream *stream = request.HTTPBodyStream;
NSMutableData *data = [[NSMutableData alloc] init];
[stream open];
while ([stream hasBytesAvailable]) {
NSInteger len = [stream read:d maxLength:1024];
if (len > 0 && stream.streamError == nil) {
[data appendBytes:(void *)d length:len];
}
}
req.HTTPBody = [data copy];
[stream close];
}
}
return req;
}
NSURLSession的加载
a、声明要实现的委托<NSURLSessionDataDelegate>
@interface NSURLProtocolViewController ()<NSURLSessionDataDelegate>
b、实现URLSessionButton
的点击方法loadNSURLSession
,不过先科普一下NSURLSession
的知识点
NSURLSeesionConfiguration
类型及属性包括:
-
defaultSessionConfiguration
:默认会话类型,能够进行磁盘缓存 -
ephemeralSessionConfiguration
: 临时会话类型,不进行任何的磁盘缓存 -
backgroundSessionConfigurationWithIdentifier
: 后台类型,用于后台下载上传
config.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;//设置缓存策略
config.networkServiceType = NSURLNetworkServiceTypeDefault;//设置网络服务类型 决定了网络请求的形式
config.timeoutIntervalForRequest = 15;//设置请求超时时间
// config.HTTPAdditionalHeaders = //设置请求头
config.allowsCellularAccess = YES;//网络属性 是否使用移动流量
创建NSURLSeesionConfiguration
,注意到一点,此处在config
中注册我们的自定义协议,之前[NSURLProtocol registerClass:[CustomURLProtocol class]];
已不再起作用,可以直接注释掉。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[CustomURLProtocol class]];
创建会话对象:delegateQueue
网络请求都是在后台进行,但是当网络请求完成后,可能会需要回到主线程进行刷新界面操作,所以此时可以设置代理回调方法所执行的队列为主队列。
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
创建并启动网络任务
NSURLSessionDataTask *task = [session dataTaskWithURL:url];
[task resume];
总的来说如下:
// 加载NSURLSession
- (void)loadNSURLSession
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[CustomURLProtocol class]];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
[dataTask resume];
}
c、实现NSURLSessionDataDelegate
的didReceiveData
方法。同时也科普一下其他方法。
didReceiveResponse:已经接收到响应时调用的代理方法
-
NSURLSessionResponseCancel
取消接受 -
NSURLSessionResponseAllow
继续接受 -
NSURLSessionResponseBecomeDownload
将当前任务转化为一个下载任务 -
NSURLSessionResponseBecomeStream
将当前任务转化为流任务
// 已经接收到响应时调用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200)
{
NSLog(@"请求成功");
NSLog(@"%@", httpResponse.allHeaderFields);// 响应头
// 初始化接收数据的NSData变量
_data = [[NSMutableData alloc] init];
//执行Block回调来继续接收响应体数据
//执行completionHandler 用于使网络连接继续接受数据
completionHandler(NSURLSessionResponseAllow);
}
else
{
NSLog(@"请求失败");
}
}
didReceiveData:接收到数据包时调用的代理方法
// 接收到数据包时调用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSLog(@"收到了一个数据包 data == %@,接受到了%li字节的数据",data,data.length);
//拼接完整数据
[_data appendData:data];
NSLog(@"拼接完后为:%@", _data);
}
didCompleteWithError:数据接收完毕时调用的代理方法
// 数据接收完毕时调用的代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(@"数据接收完成");
if (error)
{
NSLog(@"数据接收出错!");
_data = nil;// 清空出错的数据
}
else
{
//数据传输成功无误,JSON解析数据
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableLeaves error:nil];
NSLog(@"%@", dic);
}
}

d、配置
CustomURLProtocol
中的startLoading
和canInitWithRequest
方法
关于死循环了的问题:因为我们用的是NSURLSessionDataTask
发的请求还会被拦截到,拦截到后再发再拦,所以我们要对我们在startLoading
里的请求做一下标识不让它被拦截,原理就是我们在request
对象里人为的添加键值进行标识是否被处理了,如果被处理了就在canInitWithRequest
方法里返回NO
不拦截。
定义一个字符串做key
static NSString *URLProtocolHandledKey = @"URLProtocolHandledKey";
标示该request
已经处理过了,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
在canInitWithRequest
方法里返回NO
不拦截
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 发现是处理过的请求直接返回NO不拦截此请求
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request])
{
return NO;
}
return YES;
}
e、这里在startLoading
中假定一个需求:拦截网络数据,返回本地的模拟数据,进行测试
补充个关于NSURLCache
缓存的知识点:
在创建request
时,可以设置属性cachePolicy
,决定从本地还是网络上获取内容。那么如果是从本地取的话,是从哪取呢?
NSURLCahe
实现了response
的缓存机制,将NSURLRequest
和NSCachedURLResponse
映射起来。默认情况下,Memory cache=4M
,Disk cache=20M
。可以子类化NSURLCahe
实现自己的缓存逻辑。
如果response
的httpHeader
里Cache-control/expires
设置为可以被缓存,iOS会自动的将其存到本地数据库中。路径是沙盒路径下Library/Caches/bundid/Cache.db
。,对于webview
的缓存,也一样,因为它也是用的NSURLCache
。
NSCachedURLResponse
是包含了NSURLResponse
和缓存data
的类。当数据返回时,将要缓存时会调这个方法。如果返回nil
,则不缓存。
- 判断
request
的cachePolicy
是否== NSURLRequestUseProtocolCachePolicy
- 取
response
的header
,是否有cache-control
和expire
字段 - 存在
cache-control
,缓存 - 存在
expires
,缓存 -
cache-control
和expire
都没有,认为不缓存
startLoading的具体实现:不需要进行调用本地测试数据则直接继续进行网络请求,否则创建新的NSURLResponse
和NSData
,将其传给client
// 二、加载NSURLSession
- (void)startLoading
{
// 拦截的请求的request对象
NSMutableURLRequest *mutableReqeust = [self.request mutableCopy];
// 标示该request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//这个enableDebug随便根据自己的需求了,可以直接拦截到数据返回本地的模拟数据,进行测试
BOOL enableDebug = NO;
if (enableDebug)
{
NSString *str = @"测试数据";
// 将NSString转换为UTF-8数据
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
// 新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
// 将新的response作为request对应的response,不缓存
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 设置request对应的 响应数据 response data
[self.client URLProtocol:self didLoadData:data];
// 标记请求结束
[self.client URLProtocolDidFinishLoading:self];
}
else
{
//使用NSURLSession继续把request发送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
[task resume];
}
}
上面采用NSURLSession
发送的网络请求,所以实现NSURLSessionDelegate
代理方法进行回调,一般默认使用方式为:
⚠️NSURLSessionDelegate
走的是继续路线,所以需要和截取路线各自写一份client
的三个方法
#pragma mark - NSURLSessionDelegate
// 接收到返回信息时(还未开始下载), 执行的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
// 接收到服务器返回的数据 调用多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
// 打印返回数据
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr)
{
NSLog(@"***截取数据***: %@", dataStr);
}
[self.client URLProtocol:self didLoadData:data];
}
// 请求结束或者是失败的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error)
{
[self.client URLProtocol:self didFailWithError:error];
}
else
{
[self.client URLProtocolDidFinishLoading:self];
}
}
f、stopLoading
停止方法为
- (void)stopLoading
{
// NSURLSession的停止方法
[self.session invalidateAndCancel];
self.session = nil;
}
g、现在基本完成了,需要注意下控制台输出的流程,我们很明显看到,因为NSURLProtocolViewController
和CustomURLProtocol
均各自实现了一套NSURLSessionDelegate
协议以及创建NSURLSessionDataTask
,其存在明显的调用先后的顺序问题。
大致顺序如下:NSURLProtocolViewController
创建NSURLSessionDataTask
--->跳到CustomURLProtocol
执行其NSURLSessionDataTask
----->回到NSURLProtocolViewController
继续自己之前的NSURLSessionDataTask
loadNSURLSession---------黑魔法视图控制器: 加载NSURLSession
startLoading----------------自定义协议: 使用NSURLSession继续把request发送出去
didReceiveResponse--------自定义协议: 接收到返回信息时(还未开始下载)
didReceiveData-------------自定义协议: 截取数据: <!DOCTYPE html>
didCompleteWithError------ 自定义协议: 请求结束
didReceiveResponse--------黑魔法视图控制器: 请求成功
didReceiveResponse--------黑魔法视图控制器: 响应头 {
didReceiveData-------------黑魔法视图控制器: 收到了一个数据包 data
didReceiveData-------------黑魔法视图控制器: 拼接完后为 {length =
didCompleteWithError------黑魔法视图控制器: 数据接收完成
didCompleteWithError------黑魔法视图控制器: 数据传输成功无误,JSON解析数据后


h、注意一点,上面是当
enableDebug = NO
的时候,使用NSURLSession
继续把request
发送出去,并不是最初我们提到的需求直接拦截到数据返回本地的模拟数据,进行测试。
当设置enableDebug = YES
,便不会走CustomURLProtocol
的NSURLSessionDelegate
代理方法了,而是在client
拿到本地新创建的data
和response
后,直接进入NSURLProtocolViewController
的NSURLSessionDelegate
运行
BOOL enableDebug = YES;
if (enableDebug)
{
NSString *str = @"测试数据";
// 将NSString转换为UTF-8数据
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
// 新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
// 将新的response作为request对应的response,不缓存
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 设置request对应的 响应数据 response data
[self.client URLProtocol:self didLoadData:data];
// 标记请求结束
[self.client URLProtocolDidFinishLoading:self];
}
需要调整下NSURLSessionDelegate
中的方法,didReceiveResponse
删除掉之前的httpResponse
判断statusCode
状态码代码段,因为此时的response
是我们自定义的,不再是httpResponse
类型的了
// 已经接收到响应时调用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSLog(@"黑魔法视图控制器:URL---%@, expectedContentLength----%lld",response.URL, response.expectedContentLength);
_data = [[NSMutableData alloc] init];
completionHandler(NSURLSessionResponseAllow);
}
同样的,需要修改下didReceiveData
方法,将接收到的data
转化为字符串输出,可以看到控制图顺利输出了我们的data
即测试数据
字符串。
// 接收到数据包时调用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSLog(@"黑魔法视图控制器: 收到了一个数据包 data == %@,接受到了%li字节的数据",data,data.length);
//拼接完整数据
[_data appendData:data];
NSString *dataStr = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding];
NSLog(@"黑魔法视图控制器: 拼接完后为 %@", dataStr);
}

加载NSURLConnection
累死我了......
sendAsynchronousRequest:queue:completionHandler:' is deprecated: first deprecated in iOS 9.0 - Use [NSURLSession dataTaskWithRequest:completionHandler:] (see NSURLSession.h
看来苹果不支持老掉牙的NSURLConnection了......那么我还写个锤子,直接进入拦截AFNetworking
加载AFNetworking
目前为止,我们上面的代码已经能够监控到绝大部分的网络请求,但是呢,有一个却是特殊的,比如AFNetworking
请求。因为AFNetworking
网络请求的NSURLSession
实例方法都是通过
sessionWithConfiguration:delegate:delegateQueue:
方法获得的,我们是不能监听到的,
然而我们通过[NSURLSession sharedSession]
生成session
就可以拦截到请求,原因就出在NSURLSessionConfiguration
上,我们进到NSURLSessionConfiguration
里面看一下,他有一个属性:
@property (nullable, copy) NSArray<Class> *protocolClasses;
我们能够看出,这是一个NSURLProtocol
数组,上面我们提到了,我们监控网络是通过注册NSURLProtocol
来进行网络监控的,但是通过sessionWithConfiguration:delegate:delegateQueue:
得到的session
,他的configuration
中已经有一个NSURLProtocol
,所以他不会走我们的protocol
来,怎么解决这个问题呢? 其实很简单,我们将NSURLSessionConfiguration
的属性protocolClasses
的get
方法hook
掉,通过返回我们自己的protocol
,这样,我们就能够监控到通过sessionWithConfiguration:delegate:delegateQueue:
得到的session
的网络请求。
所以对于AFNetworking
中网络请求初始化方法可以修改为:
// 加载AFNetworking
- (void)loadAFNetworking
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
//指定其protocolClasses
configuration.protocolClasses = @[[CustomURLProtocol class]];
// 不采用manager初始化,改为以下方式
//AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:configuration];
[manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
}];
}
很简单是吧,看看运行结果是否真的实现了,如果是应该也能和NSURLSession
一样拦截成功,输出自定义协议CustomURLProtocol
中NSURLSessionDelegate
的一大堆东西,但是与加载NSURLSession
不同的是,因为变成了AFNetworking
,所以不会打印出NSURLProtocolViewController
中NSURLSessionDelegate
的一大堆东西。

拓展:使用runtime来实现加载AFNetworking
a、新建一个FFSessionConfiguration
类,作为我们自定义的SessionConfiguration
,用来做方法交换
b、首先在该类中创建一个单例,用来在其他类中调用交换方法和还原方法
// 单例
+ (FFSessionConfiguration *)defaultConfiguration
{
static FFSessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration = [[FFSessionConfiguration alloc] init];
});
return staticConfiguration;
}
c、创建一个isExchanged
属性,用于判断是否已经交换过了,现在对它进行初始化为NO
// 初始化
- (instancetype)init
{
self = [super init];
if (self) {
self.isExchanged = NO;
}
return self;
}
d、实现一个交换两个类中同一个方法名的具体实现的方法,即swizzleSelector
来实现方法混淆,此处需要引入#import <objc/runtime.h>
// 交换两个方法,此处运用到runtime
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub
{
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
// 有一个找不到就抛出异常
if (!originalMethod || !stubMethod)
{
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
// 交换二者的实现方法,即方法混淆
method_exchangeImplementations(originalMethod, stubMethod);
}
e、然后实现我们需要交换的那个同名方法,即protocolClasses
// 如果还有其他的监控protocol,也可以在这里加进去
// 此处用到了CustomURLProtocol
- (NSArray *)protocolClasses
{
return @[[CustomURLProtocol class]];
}
f、我们最终的目的是要将NSURLSessionConfiguration
和 FFSessionConfiguration
中的protocolClasses
方法进行交换,于是写出我们的核心方法load
// 交换掉 NSURLSessionConfiguration的protocolClasses方法
- (void)load
{
// 是否交换方法 YES
self.isExchanged = YES;
// NSURLSessionConfiguration
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 将NSURLSessionConfiguration 和 FFSessionConfiguration中的protocolClasses方法进行交换
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
g、最后需要有还原为初始化状态,不再拦截的方法unload
// 还原初始化
- (void)unload
{
// 是否交换方法 NO
self.isExchanged = NO;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 再替换一次就回来了
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
综上,.m
文件如下:
#import "FFSessionConfiguration.h"
#import <objc/runtime.h>
#import "CustomURLProtocol.h"
@implementation FFSessionConfiguration
// 单例
+ (FFSessionConfiguration *)defaultConfiguration
{
static FFSessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration = [[FFSessionConfiguration alloc] init];
});
return staticConfiguration;
}
// 初始化
- (instancetype)init
{
self = [super init];
if (self) {
self.isExchanged = NO;
}
return self;
}
// 交换掉 NSURLSessionConfiguration的protocolClasses方法
- (void)load
{
// 是否交换方法 YES
self.isExchanged = YES;
// NSURLSessionConfiguration
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 将NSURLSessionConfiguration 和 FFSessionConfiguration中的protocolClasses方法进行交换
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
// 还原初始化
- (void)unload
{
// 是否交换方法 NO
self.isExchanged = NO;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
// 再替换一次就回来了
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
// 交换两个方法,此处运用到runtime
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub
{
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
// 有一个找不到就抛出异常
if (!originalMethod || !stubMethod)
{
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
// 交换二者的实现方法,即方法混淆
method_exchangeImplementations(originalMethod, stubMethod);
}
// 如果还有其他的监控protocol,也可以在这里加进去
// 此处用到了CustomURLProtocol
- (NSArray *)protocolClasses
{
return @[[CustomURLProtocol class]];
}
@end
而提供给外界的接口.h
文件如下:
@interface FFSessionConfiguration : NSObject
@property (nonatomic,assign) BOOL isExchanged;// 是否交换方法
+ (FFSessionConfiguration *)defaultConfiguration;// 单例
// 交换掉NSURLSessionConfiguration的 protocolClasses方法
- (void)load;
// 还原初始化
- (void)unload;
@end
还好,这个方法混淆的实现并没有我想象的困难。接下来在NSURLProtocolViewController
看下具体如何使用。
a、我们需要实现一个方法来取得单例并在判断没有交换后进行protocolClasses
的交换。
// 开始监听
+ (void)startMonitor
{
// 取得单例
FFSessionConfiguration *sessionConfiguration = [FFSessionConfiguration defaultConfiguration];
// 注册
[NSURLProtocol registerClass:[CustomURLProtocol class]];
// 还没有交换就交换
if (![sessionConfiguration isExchanged])
{
// 交换
[sessionConfiguration load];
}
}
b、同样地,我们也需要实现一个类似方法来取消交换
// 停止监听
+ (void)stopMonitor
{
// 取得单例
FFSessionConfiguration *sessionConfiguration = [FFSessionConfiguration defaultConfiguration];
// 当不需要拦截的时候,要进行注销
[NSURLProtocol unregisterClass:[CustomURLProtocol class]];
// 已经交换过了就还原
if ([sessionConfiguration isExchanged])
{
// 还原
[sessionConfiguration unload];
}
}
然后进入关键的NSURLProtocolViewController
中来实现调用。
a、首先因为我们在startMonitor
已经注册过了,所以需要注释掉之前viewDidLoad
中的[NSURLProtocol registerClass:[CustomURLProtocol class]];
,直接改为[CustomURLProtocol startMonitor];
即可
- (void)viewDidLoad
{
[super viewDidLoad];
[self createSubviews];
// 注册NSURLProtocol的子类
// 当NSURLSeesionConfiguration使用protocolClasses注册的时候,此处不再起作用,可以直接注释掉
// 当使用runtime拦截AFNetworking时,此处也需要注释掉,因为在自定义协议里已经配置过了
// [NSURLProtocol registerClass:[CustomURLProtocol class]];
// 使用runtime拦截AFNetworking时,使用这句话
[CustomURLProtocol startMonitor];
}
b、同样的原因,dealloc
也做相应修改
- (void)dealloc
{
// 一经注册之后,所有交给URL Loading system的网络请求都会被拦截,所以当不需要拦截的时候,要进行注销
// 当使用runtime拦截AFNetworking时,此处也需要注释掉,因为在自定义协议里已经配置过了
// [NSURLProtocol unregisterClass:[CustomURLProtocol class]];
// 使用runtime拦截AFNetworking时,使用这句话
[CustomURLProtocol stopMonitor];
}
c、接下来最后一步啦,实现AFNetworkingRuntimeButton
的runtimeLoadAFNetworking
方法,为什么需要这个方法呢?因为之前针对AFNetworking
,我们的拦截方式是将NSURLSessionConfiguration
的属性protocolClasses
的get
方法hook
掉,通过返回我们自己的protocol
,而且也不采用manager
初始化:
// 加载AFNetworking
- (void)loadAFNetworking
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
//指定其protocolClasses
configuration.protocolClasses = @[[CustomURLProtocol class]];
// 不采用manager初始化,改为以下方式
//AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:configuration];
[manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
}];
}
这些我们统统需要进行修改,即删除不必要的东西,使用默认的manager
进行初始化,结果如下:
// runtime加载AFNetworking
- (void)runtimeLoadAFNetworking
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
}];
}

大功告成!各位兄弟姐妹,父老乡亲,这篇内容实在有点儿庞杂,希望各位跟着我的demo认真研读。
参考文献
NSURLCache缓存的位置
iOS中NSURLProtocol黑魔法的使用
NSURLProtocol对WKWebView的处理