欢迎访问个人博客原文git
NSURLProtocol 是 Foundation 框架中 URL Loading System 的一部分。它可让开发者能够在不修改应用内原始请求代码的状况下,去改变 URL 加载的所有细节。换句话说,NSURLProtocol 是一个被 Apple 默许的中间人攻击。github
虽然 NSURLProtocol 叫“Protocol”,却不是协议,而是一个抽象类。web
既然 NSURLProtocol 是一个抽象类,说明它没法被实例化,那么它又是如何实现网络请求拦截的?算法
答案就是经过子类化来定义新的或是已经存在的 URL 加载行为。若是当前的网络请求是能够被拦截的,那么开发者只须要将一个自定义的 NSURLProtocol 子类注册到 App 中,在这个子类中就能够拦截到全部请求并进行修改。数组
那么到底哪些网络请求能够被拦截?缓存
前面已经说了,NSURLProtocol 是 URL Loading System 的一部分,因此它能够拦截全部基于 URL Loading System 的网络请求:服务器
相应的,基于它们实现的第三方网络框架 AFNetworking 和 Alamofire 的网络请求,也能够被 NSURLProtocol 拦截到。markdown
但早些年基于 CFNetwork 实现的,好比 ASIHTTPRequest,其网络请求就没法被拦截。网络
另外,UIWebView 也是能够被 NSURLProtocol 拦截的,但 WKWebView 不能够。(由于 WKWebView 是基于 WebKit,并不走 C socket。)session
所以,在实际应用中,它的功能十分强大,好比:
下面来看一下 NSURLProtocol 的相关方法。
// 建立一个 URL 协议实例来处理 request 请求 - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client; // 建立一个 URL 协议实例来处理 session task 请求 - (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client; 复制代码
// 尝试注册 NSURLProtocol 的子类,使之在 URL 加载系统中可见 + (BOOL)registerClass:(Class)protocolClass; // 注销 NSURLProtocol 的指定子类 + (void)unregisterClass:(Class)protocolClass; 复制代码
子类化 NSProtocol 的首要任务就是告知它,须要控制什么类型的网络请求。
// 肯定协议子类是否能够处理指定的 request 请求,若是返回 YES,请求会被其控制,返回 NO 则直接跳入下一个 protocol + (BOOL)canInitWithRequest:(NSURLRequest *)request; // 肯定协议子类是否能够处理指定的 task 请求 + (BOOL)canInitWithTask:(NSURLSessionTask *)task; 复制代码
NSURLProtocol 容许开发者去获取、添加、删除 request 对象的任意元数据。这几个方法经常使用来处理请求无限循环的问题。
// 在指定的请求中获取与指定键关联的属性 + (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request; // 设置与指定请求中的指定键关联的属性 + (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request; // 删除与指定请求中的指定键关联的属性 + (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request; 复制代码
若是你想要用特定的某个方式来修改请求,能够用下面这个方法。
// 返回指定请求的规范版本 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request; 复制代码
// 判断两个请求是否相同,若是相同可使用缓存数据,一般只须要调用父类的实现 + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b; 复制代码
这是子类中最重要的两个方法,不一样的自定义子类在调用这两个方法时会传入不一样的内容,但共同点都是围绕 protocol 客户端进行操做。
// 开始加载 - (void)startLoading; // 中止加载 - (void)stopLoading; 复制代码
// 获取协议接收者的缓存 - (NSCachedURLResponse *)cachedResponse; // 接受者用来与 URL 加载系统通讯的对象,每一个 NSProtocol 的子类实例都拥有它 - (id<NSURLProtocolClient>)client; // 接收方的请求 - (NSURLRequest *)request; // 接收方的任务 - (NSURLSessionTask *)task; 复制代码
NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL 转发。先来看如何拦截网络请求。
这里建立一个名为 HTCustomURLProtocol
的子类。
@interface HTCustomURLProtocol : NSURLProtocol @end 复制代码
在合适的位置注册这个子类。对基于 NSURLConnection 或者使用 [NSURLSession sharedSession]
初始化对象建立的网络请求,调用 registerClass
方法便可。
[NSURLProtocol registerClass:[NSClassFromString(@"HTCustomURLProtocol") class]]; // 或者 // [NSURLProtocol registerClass:[HTCustomURLProtocol class]]; 复制代码
若是须要全局监听,能够设置在 AppDelegate.m
的 didFinishLaunchingWithOptions
方法中。若是只须要在单个 UIViewController 中使用,记得在合适的时机注销监听:
[NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]]; 复制代码
若是是基于 NSURLSession 的网络请求,且不是经过 [NSURLSession sharedSession]
方式建立的,就得配置 NSURLSessionConfiguration 对象的 protocolClasses
属性。
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfiguration.protocolClasses = @[[NSClassFromString(@"HTCustomURLProtocol") class]]; 复制代码
实现子类分为五个步骤:
注册 → 拦截 → 转发 → 回调 → 结束
以拦截 UIWebView 为例,这里须要重写父类的这五个核心方法。
// 定义一个协议 key static NSString * const HTCustomURLProtocolHandledKey = @"HTCustomURLProtocolHandledKey"; // 在拓展中定义一个 NSURLConnection 属性。经过 NSURLSession 也能够拦截,这里只是以 NSURLConnection 为例。 @property (nonatomic, strong) NSURLConnection *connection; // 定义一个可变的请求返回值, @property (nonatomic, strong) NSMutableData *responseData; // 方法 1:在拦截到网络请求后会调用这一方法,能够再次处理拦截的逻辑,好比设置只针对 http 和 https 的请求进行处理。 + (BOOL)canInitWithRequest:(NSURLRequest *)request { // 只处理 http 和 https 请求 NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) { // 看看是否已经处理过了,防止无限循环 if ([NSURLProtocol propertyForKey:HTCustomURLProtocolHandledKey inRequest:request]) { return NO; } // 若是还须要截取 DNS 解析请求中的连接,能够继续加判断,是否为拦截域名请求的连接,若是是返回 NO return YES; } return NO; } // 方法 2:【关键方法】能够在此对 request 进行处理,好比修改地址、提取请求信息、设置请求头等。 + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { // 能够打印出全部的请求连接包括 CSS 和 Ajax 请求等 NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString); NSMutableURLRequest *mutableRequest = [request mutableCopy]; return mutableRequest; } // 方法 3:【关键方法】在这里设置网络代理,从新建立一个对象将处理过的 request 转发出去。这里对应的回调方法对应 <NSURLProtocolClient> 协议方法 - (void)startLoading { // 能够修改 request 请求 NSMutableURLRequest *mutableRequest = [[self request] mutableCopy]; // 打 tag,防止递归调用 [NSURLProtocol setProperty:@YES forKey:HTCustomURLProtocolHandledKey inRequest:mutableRequest]; // 也能够在这里检查缓存 // 将 request 转发,对于 NSURLConnection 来讲,就是建立一个 NSURLConnection 对象;对于 NSURLSession 来讲,就是发起一个 NSURLSessionTask。 self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self]; } // 方法 4:主要判断两个 request 是否相同,若是相同的话可使用缓存数据,一般只须要调用父类的实现。 + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } // 方法 5:处理结束后中止相应请求,清空 connection 或 session - (void)stopLoading { if (self.connection != nil) { [self.connection cancel]; self.connection = nil; } } // 按照在上面的方法中作的自定义需求,看状况对转发出来的请求在恰当的时机进行回调处理。 #pragma mark- NSURLConnectionDelegate - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } #pragma mark - NSURLConnectionDataDelegate // 当接收到服务器的响应(连通了服务器)时会调用 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.responseData = [[NSMutableData alloc] init]; // 能够处理不一样的 statusCode 场景 // NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; // 能够设置 Cookie [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } // 接收到服务器的数据时会调用,可能会被调用屡次,每次只传递部分数据 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.responseData appendData:data]; [self.client URLProtocol:self didLoadData:data]; } // 服务器的数据加载完毕后调用 - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } // 请求错误(失败)的时候调用,好比出现请求超时、断网,通常指客户端错误 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } 复制代码
上面用到的一些 NSURLProtocolClient 方法:
@protocol NSURLProtocolClient <NSObject> // 请求重定向 - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; // 响应缓存是否合法 - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse; // 刚接收到 response 信息 - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; // 数据加载成功 - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data; // 数据完成加载 - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol; // 数据加载失败 - (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error; // 为指定的请求启动验证 - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; // 为指定的请求取消验证 - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; @end 复制代码
若是在 NSURLProtocol 中使用 NSURLSession,须要注意:
registerClass
注册,只能经过 [NSURLSession sharedSession]
的方式建立网络请求。当有多个自定义 NSURLProtocol 子类注册到系统中的话,会按照他们注册的反向顺序依次调用 URL 加载流程,也就是最后注册的 NSURLProtocol 会被优先判断。
对于经过配置 NSURLSessionConfiguration 对象的 protocolClasses
属性来注册的状况,protocolClasses
数组中只有第一个 NSURLProtocol 会起做用,后续的 NSURLProtocol 就没法拦截到了。
因此 OHHTTPStubs 在注册 NSURLProtocol 子类的时候是这样处理的:
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig { // Runtime check to make sure the API is available on this version if ([sessionConfig respondsToSelector:@selector(protocolClasses)] && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]) { NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; Class protoCls = HTTPStubsProtocol.class; if (enable && ![urlProtocolClasses containsObject:protoCls]) { // 将本身的 NSURLProtocol 插入到 protocolClasses 的第一个,进行拦截 [urlProtocolClasses insertObject:protoCls atIndex:0]; } else if (!enable && [urlProtocolClasses containsObject:protoCls]) { // 拦截完成后移除 [urlProtocolClasses removeObject:protoCls]; } sessionConfig.protocolClasses = urlProtocolClasses; } else { NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. " @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call " @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd)); } } 复制代码
虽然 NSURLProtocol 没法直接拦截 WKWebView,但其实仍是有解决方案的。就是使用 WKBrowsingContextController
和 registerSchemeForCustomProtocol
。
// 注册 scheme Class cls = NSClassFromString(@"WKBrowsingContextController"); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([cls respondsToSelector:sel]) { // 经过 http 和 https 的请求,同理可经过其余的 Scheme 可是要知足 URL Loading System [cls performSelector:sel withObject:@"http"]; [cls performSelector:sel withObject:@"https"]; } 复制代码
但因为这涉及到了私有方法,直接引用没法过苹果的机审,因此使用的时候须要对字符串作下处理,好比对方法名进行算法加密处理等,实测也是能够经过审核的。
总之,NSURLProtocol 很是强大,不管是优化 App 的性能,仍是拓展功能,都具备很强的可塑空间,但在使用的同时,又要多关注它带来的问题。尽管它在不少框架或者知名项目中都已经得以应用,其奥义依然值得开发者们去深刻研究。