概述
先说明下iOS中加载url的正常流程:
1.客户端发送NSURLRequest给server
2.server返回对应的NSURLResponse
如果被WebViewProxy拦截,则流程变为:
1.客户端发送NSURLRequest给server
2.这个request被WebViewProxy拦截
3.proxy将修改后的新request发送给server
4.server返回response给proxy
5.proxy将返回的数据以url response或者回调的形式返回给客户端。
那么WebViewProxy的拦截原理是怎样的呢?
拦截原理
首先,WebViewProxy定义一个自定义的protocol(NSURLProtocol的子类)
@interface WebViewProxyURLProtocol : NSURLProtocol
题外话,子类必须实现NSURLProtocol的以下几个方法才能正常工作,当然这部分工作WebViewProxy已经都帮我们搞定了:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
- (void)startLoading;
- (void)stopLoading;
然后把URLProtocol的子类注册到url loading system中
+ (void)initialize {
...
[NSURLProtocol registerClass:[WebViewProxyURLProtocol class]];
}
这样,以后app每次发送url request时都检查此request是否适用于WebViewProxyURLProtocol(过滤条件也是用户定义的),如果符合筛选条件,则使用WebViewProxyURLProtocol来加载该url。
这里需要注意下,原始的request和修改后的新request,可能都符合拦截条件,所以为了不使其无限制的拦截并循环处理下去,需要设置一个检测条件,保证每个request至多被处理一次。流程见下图。
拦截的原理依据如下(NSURLProtocol的registerClass方法):
+ (BOOL)registerClass:(Class)protocolClass
When the URL loading system begins to load a request, each registered protocol class is consulted in turn to see if it can be initialized with the specified request. The first NSURLProtocol subclass to return YES when sent a canInitWithRequest: message is used to perform the URL load. There is no guarantee that all registered protocol classes will be consulted.
Classes are consulted in the reverse order of their registration.
使用举例
简单的拦截例子:
[WebViewProxy handleRequestsMatching:[NSPredicate predicateWithFormat:@"host MATCHES[cd] '[foo|bar]'"] handler:^(NSURLRequest* req, WVPResponse *res) {
[res respondWithText:@"Hi!"];
}];
简单的转发例子:
[WebViewProxy handleRequestsWithHost:@"example.proxy" handler:^(NSURLRequest *req, WVPResponse *res) {
NSString* proxyUrl = [req.URL.absoluteString stringByReplacingOccurrencesOfString:@"example.proxy" withString:@"example.com"];
NSURLRequest* proxyReq = [NSURLRequest requestWithURL:[NSURL URLWithString:proxyUrl]];
[NSURLConnection connectionWithRequest:proxyReq delegate:res];
}];
更多详见https://github.com/marcuswestin/WebViewProxy。
值得注意的是处理server返回的response时,有三套api可供选择:
High level API返回image, text, html or json
Low level API返回HTTP头和NSData
Piping API:从NSURLConnection返回data/error
注意事项:
注册自定义protocol可能和其他模块或sdk的拦截功能冲突,导致拦截无效,依据是上文中的:The first NSURLProtocol subclass to return YES when sent a canInitWithRequest: message is used to perform the URL load.
WebViewProxy每次拦截成功后,都会在请求的url尾部加上一个fragment后缀(#__webviewproxyreq__)用来标记该url已拦截,防止下次再次拦截从而造成死循环。这样有个隐患,就是url会被污染,可能影响某些正常功能。
一个解决方案是在http头中增加一个标记字段来表示该url已经被拦截过,从而跳出循环。
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
...
// 原有逻辑,注释掉
//NSString* correctedFragment;
//if (_correctedRequest.URL.fragment) {
// correctedFragment = @"__webviewproxyreq__";
//} else {
// correctedFragment = @"#__webviewproxyreq__";
//}
//_correctedRequest.URL = [NSURL URLWithString:[request.URL.absoluteString stringByAppendingString:correctedFragment]];
//使用http头来标记的新逻辑 Add by zhouyi.
[_correctedRequest addValue:[@(YES) stringValue] forHTTPHeaderField:webViewProxyFlagKey];
...
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
...
// 新增标记字段,表示已经被WebViewProxy处理过
// Add by zhouyi.
NSString* proxyFlag = request.allHTTPHeaderFields[webViewProxyFlagKey];
if (proxyFlag)
{
return NO;
}
// 这是原有逻辑,注释掉
//if ([webViewProxyLoopDetection evaluateWithObject:request.URL])
//{
// return NO;
//}
...
}