在iOS中使用NSURLProtocol进行网络代理
一 引言
网络能力是互联网应用程序必不可少的功能。随着应用程序的复杂,对网络的依赖性也会逐渐增高。如何统一的处理请求头,统一的处理回执数据,统一的进行网络请求过程的监控和修改等都是开发者要考虑的处理的问题。
通常,对于新项目,我们会统一封装网络框架在处理应用中的请求,整个网络的发起和收到回执的过程都可以很好的在底层的框架中进行监控和数据处理。但是这种方式对于网络请求不统一的老项目可能成本较高,要统一的修改网络框架,且对于WebView中的网络请求也需要单独处理,比较繁琐。这种情况下,不妨试一试Foundation框架中自带的NSURLProtocol来进行网络代理,完全无侵入的实现网络全过程的监控和修改处理。
二 牛刀小试
在系统的介绍NSURLProtocol之前,我们先来通过一个小例子体验下其使用过程。
首先,NSURLProtocol虽然名字中有Protocol,但是它并不是一个协议,其是继承于NSObject的类。其次,虽然其实一个继承为NSObejct的类,但它更像是一个抽象类,我们不会直接拿这个类进行使用,而是会通过子类的方式来实现它,并且其内的很多方法也都是抽象的,必须由子类来实现。
我们新建一个测试工程,先实现一个简单的GET请求,如下:
1 2 3 4 5
| NSURL *url = [NSURL URLWithString: @"https://www.baidu.com"]; [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@", response); }] resume] ;
|
运行代码,通过控制台的输出,可以看到能够正常的获取到回执数据。
新建一个命名为NetworkProtocl的类,使其继承自NSURLProtocol,实现如下:
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 35 36 37 38 39 40 41
| #import "NetworkProtcol.h"
@implementation NetworkProtcol
+ (void)load { [NSURLProtocol registerClass:self]; }
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { NSLog(@"canInitWithRequest: %@", request.URL.absoluteString); if ([self propertyForKey:@"Handle" inRequest:request]) { return false; } return YES; }
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { NSLog(@"canonicalRequestForRequest: %@", request.URL.absoluteString); return request; }
- (void)startLoading { NSLog(@"startLoading"); [NSURLProtocol setProperty:@YES forKey:@"Handle" inRequest:self.request]; [[[NSURLSession sharedSession] dataTaskWithRequest:self.request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; [self.client URLProtocolDidFinishLoading:self]; }] resume]; }
- (void)stopLoading { NSLog(@"stopLoading"); }
@end
|
再次运行,通过控制台的打印,可以看到网络代理已经可以正常的工作,如上面的示例代码所示,整个代理过程最重要的即是三步:
1. 判断某个请求是否要进行代理拦截。
2. 处理请求,可以进行修改。
3. 执行真正的拦截行为,并通过回调来返回结果给原请求方。
三 NSURLProtocol详解
NSURLProtocol本身比较简单,其暴露的接口和属性也比较简洁,解释如下:
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 35 36 37 38 39 40 41 42 43 44 45 46 47
| @interface NSURLProtocol : NSObject
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client; - (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client;
@property (nullable, readonly, copy) NSURLSessionTask *task
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
@property (readonly, copy) NSURLRequest *request;
@property (nullable, readonly, copy) NSCachedURLResponse *cachedResponse;
+ (BOOL)canInitWithRequest:(NSURLRequest *)request; + (BOOL)canInitWithTask:(NSURLSessionTask *)task;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
+ (BOOL)registerClass:(Class)protocolClass;
+ (void)unregisterClass:(Class)protocolClass;
@end
|
其中,client属性用来与原请求方交互,其协议的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @protocol NSURLProtocolClient <NSObject>
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
- (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
|
四 对网页视图的网络请求进行拦截
如果是使用UIWebView加载网页,则NSURLProtocol默认支持拦截,示例代码如下:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #import "NetworkProtcol.h"
@interface NetworkProtcol ()<NSURLSessionDelegate>
@property (atomic,strong,readwrite) NSURLSessionDataTask *task; @property (nonatomic,strong) NSURLSession *session;
@end
@implementation NetworkProtcol
+ (void)load { [NSURLProtocol registerClass:self]; }
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) { if ([NSURLProtocol propertyForKey:@"Handle" inRequest:request]) { return NO; } return YES; } return NO; }
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { return request; } - (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; [NSURLProtocol setProperty:@YES forKey:@"Handle" inRequest:mutableReqeust]; NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; self.session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:queue]; self.task = [self.session dataTaskWithRequest:mutableReqeust]; [self.task resume]; }
- (void)stopLoading { [self.session invalidateAndCancel]; self.session = nil; }
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error != nil) { [self.client URLProtocol:self didFailWithError:error]; }else { [self.client URLProtocolDidFinishLoading:self]; } }
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; completionHandler(NSURLSessionResponseAllow); }
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; }
@end
|
如果使用的是WKWebView,则无法直接被拦截,需要做如下的额外处理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class]; SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([cls respondsToSelector:sel]) { [cls performSelector:sel withObject:@"http"]; [cls performSelector:sel withObject:@"https"]; }
WKWebView *web = [[WKWebView alloc] initWithFrame:self.view.frame]; [self.view addSubview:web]; NSURL *url = [NSURL URLWithString: @"https://www.baidu.com"]; [web loadRequest:[NSURLRequest requestWithURL:url]];
|
需要注意,这里会涉及到一些私有属性和方法,可能会存在提审风险。
五 一些思考
NSURLProtocol的使用非常简单,但是可以做的事情却并不少。例如我们可以用其来做统一的网络处理,来做无侵入的网络监控等。
- 作为底层工具,统一的为Request添加通用Header字段。
- 统一的进行自定义用户认证。
- 做为环境切换工具,根据环境做URL的映射。
- 请求参数的整理和修改,统一增加通用参数。
- 统一修改或增加Response Header数据。
- 根据需求,做为本地的Mock工具,访问到本地服务或Mock远程数据。
- 监控端到端的网络请求性能,进行时间统计。
- 等等…
专注技术,懂的热爱,愿意分享,做个朋友
QQ:316045346