初衷是為了讓 UIWebView 可以攔截 Ajax 請求。研究了一番找到了 NSURLProtocol。
NSURLProtocol 是屬于 Foundation 框架里的 URL Loading System 的一部分,它是一個抽象類,不能去實例化它,只能子類化NSURLProtocol,然后使用的時候注冊子類。一個相對晦澀難解的類。
那么如果開發者自定義的一個 NSURLProtocol 并且注冊到 app 中,那么在這個自定義的 NSURLProtocol 中我們可以攔截所有的請求,進行修改,或者修改 response。
NSHipster 上說:「或者這么說吧: NSURLProtocol 就是一個蘋果允許的中間人攻擊。」
能做什么?
- 重定向網絡請求(可以解決之前電信的 DNS 域名劫持問題)
- 緩存
- 自定義 Response (過濾敏感信息)
- 全局網絡請求設置
- HTTP Mocking
使用方法
1. 子類化:
由于 NSURLProtocol 是一個抽象類,所以使用的時候必須定義一個它的子類:
#import <Foundation/Foundation.h>
@interface ZDYURLProtocol : NSURLProtocol
@end
2. 注冊:
[NSURLProtocol registerClass:[ZDYURLProtocol class]];
NSURLConnection 發起請求的時候,會讓所有已注冊的 URLProtocol 來“審批”這個請求
注意: 如果是基于 NSURLSession 進行的請求,注冊的時候需要注冊到 NSURLSessionConfiguration 中:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[[ZDYURLProtocol class]];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:request];
[task resume];
記得用完之后注銷:
[NSURLProtocol unregisterClass:[MyURLProtocol class]];
3. 抽象對象必須實現的方法
注冊成功之后,就需要我們的子類去實現抽象方法:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
canInitWithRequest
用來審批
的方法。
前面說到「NSURLConnection 發起請求的時候,會讓所有已注冊的 URLProtocol 來“審批”這個請求」。這里返回NO代表放過這個請求,不作處理。返回YES,代表需要處理,則會進入后續的流程。
注意:這里需要放過已經處理過的請求:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//處理過的,放過
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
// 你的邏輯代碼
return NO;
}
canonicalRequestForRequest
這個方法用來統一處理請求 request 對象的,可以修改頭信息,或者重定向。沒有特殊需要,則直接return request;
如果要在這里做重定向以及頭信息的時候注意檢查是否已經添加,因為這個方法可能被調用多次,也可以在后面的方法中做。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
requestIsCacheEquivalent
判斷網絡請求是否一致,一致的話使用緩存數據。沒需要就調用 super 的方法。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
startLoading
子類中最重要的方法就是 -startLoading 和 -stopLoading,實現請求和取消流程。不同的自定義子類在調用這兩個方法是會傳入不同的內容,但共同點都是要圍繞 protocol 客戶端進行操作。
可以在這里修改請求信息,重定向,DNS解析,返回自定義的測試數據。
重點:需要標記已經處理過的 request:
- (void)startLoading {
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//request處理過的放進去
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
URLProtocolHandledKey 是:
static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";
舉例:直接在 startLoading
中返回測試數據:
NSData *data = [@"testData" dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
stopLoading
- (void)stopLoading {
[self.connection cancel];
self.connection = nil;
}
4. 攔截之后的處理過程
需要注意的是父類中有一個 client 屬性。
/*!
@method client
@abstract Returns the NSURLProtocolClient of the receiver.
@result The NSURLProtocolClient of the receiver.
*/
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
實現的協議 <NSURLProtocolClient>
如下:
- (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 didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
對于需要處理的 connection,可以在下的 NSURLConnectionDataDelegate 中進行操作:
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)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];
}
注意事項:
如果我們順序注冊 A B C 三個 Protocol,那么一個 connection 在發送的時候,處理的順序是 C B A,而且最多只有一個 Protocol 會觸發處理。
攔截 UIWebview 的請求,會有被拒的風險。
注意標記處理過的,具體做法在本文搜關鍵詞 URLProtocolHandledKey
。