这篇文章会提供一种在 Cocoa 层拦截全部 HTTP 请求的方法,其实标题已经说明了拦截 HTTP 请求须要的了解的就是 NSURLProtocol
。html
因为文章的内容较长,会分红两部分,这篇文章介绍 NSURLProtocol
拦截 HTTP 请求的原理,另外一篇文章如何进行 HTTP Mock 介绍这个原理在 OHHTTPStubs
中的应用,它是如何 Mock(伪造)某个 HTTP 请求对应的响应的。ios
NSURLProtocol
是苹果为咱们提供的 URL Loading System 的一部分,这是一张从官方文档贴过来的图片:git
官方文档对 NSURLProtocol
的描述是这样的:github
An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.缓存
在每个 HTTP 请求开始时,URL 加载系统建立一个合适的 NSURLProtocol
对象处理对应的 URL 请求,而咱们须要作的就是写一个继承自 NSURLProtocol
的类,并经过 - registerClass:
方法注册咱们的协议类,而后 URL 加载系统就会在请求发出时使用咱们建立的协议对象对该请求进行处理。网络
这样,咱们须要解决的核心问题就变成了如何使用 NSURLProtocol
来处理全部的网络请求,这里使用苹果官方文档中的 CustomHTTPProtocol 进行介绍,你能够点击这里下载源代码。session
在这个工程中 CustomHTTPProtocol.m
是须要重点关注的文件,CustomHTTPProtocol
就是 NSURLProtocol
的子类:app
@interface CustomHTTPProtocol : NSURLProtocol
...
@end复制代码
如今从新回到须要解决的问题,也就是 如何使用 NSURLProtocol 拦截 HTTP 请求?,有这个么几个问题须要去解决:socket
NSURLProtocol
如何实例化?上面的这几个问题其实均可以经过 NSURLProtocol
为咱们提供的 API 来解决,决定请求是否须要当前协议对象处理的方法是:+ canInitWithRequest
:ide
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
BOOL shouldAccept;
NSURL *url;
NSString *scheme;
shouldAccept = (request != nil);
if (shouldAccept) {
url = [request URL];
shouldAccept = (url != nil);
}
return shouldAccept;
}复制代码
由于项目中的这个方法是大约有 60 多行,在这里只粘贴了其中的一部分,只为了说明该方法的做用:每一次请求都会有一个 NSURLRequest
实例,上述方法会拿到全部的请求对象,咱们就能够根据对应的请求选择是否处理该对象;而上面的代码只会处理全部 URL
不为空的请求。
请求通过 + canInitWithRequest:
方法过滤以后,咱们获得了全部要处理的请求,接下来须要对请求进行必定的操做,而这都会在 + canonicalRequestForRequest:
中进行,虽然它与 + canInitWithRequest:
方法传入的 request 对象都是一个,可是最好不要在 + canInitWithRequest:
中操做对象,可能会有语义上的问题;因此,咱们须要覆写 + canonicalRequestForRequest:
方法提供一个标准的请求对象:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}复制代码
这里对请求不作任何修改,直接返回,固然你也能够给这个请求加个 header,只要最后返回一个 NSURLRequest
对象就能够。
在获得了须要的请求对象以后,就能够初始化一个 NSURLProtocol
对象了:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}复制代码
在这里直接调用 super
的指定构造器方法,实例化一个对象,而后就进入了发送网络请求,获取数据并返回的阶段了:
- (void)startLoading {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request];
[task resume];
}复制代码
这里使用简化了 CustomHTTPClient 中的项目代码,能够达到几乎相同的效果。
你能够在 - startLoading
中使用任何方法来对协议对象持有的 request
进行转发,包括 NSURLSession
、 NSURLConnection
甚至使用 AFNetworking 等网络库,只要你能在回调方法中把数据传回 client
,帮助其正确渲染就能够,好比这样:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}复制代码
固然这里省略后的代码只会保证大多数状况下的正确执行,只是给你一个对获取响应数据粗略的认知,若是你须要更加详细的代码,我以为最好仍是查看一下
CustomHTTPProtocol
中对 HTTP 响应处理的代码,也就是NSURLSessionDelegate
协议实现的部分。
client
你能够理解为当前网络请求的发起者,全部的 client
都实现了 NSURLProtocolClient
协议,协议的做用就是在 HTTP 请求发出以及接受响应时向其它对象传输数据:
@protocol NSURLProtocolClient <NSObject>
...
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
...
@end复制代码
固然这个协议中还有不少其余的方法,好比 HTTPS 验证、重定向以及响应缓存相关的方法,你须要在合适的时候调用这些代理方法,对信息进行传递。
若是你只是继承了 NSURLProtocol
而且实现了上述方法,依然不能达到预期的效果,完成对 HTTP 请求的拦截,你还须要在 URL 加载系统中注册当前类:
[NSURLProtocol registerClass:self];复制代码
须要注意的是
NSURLProtocol
只能拦截UIURLConnection
、NSURLSession
和UIWebView
中的请求,对于WKWebView
中发出的网络请求也无能为力,若是真的要拦截来自WKWebView
中的请求,仍是须要实现WKWebView
对应的WKNavigationDelegate
,并在代理方法中获取请求。 不管是NSURLProtocol
、NSURLConnection
仍是NSURLSession
都会走底层的 socket,可是WKWebView
可能因为基于 WebKit,并不会执行 C socket 相关的函数对 HTTP 请求进行处理,具体会执行什么代码暂时不是很清楚,若是对此有兴趣的读者,能够联系笔者一块儿讨论。
若是你只想了解如何对 HTTP 请求进行拦截,其实看到这里就能够了,不过若是你想应用文章中的内容或者但愿了解如何伪造 HTTP 响应,能够看下一篇文章如何进行 HTTP Mock。
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · Github
Source: draveness.me/intercept