最近在專案裡由於電信那邊發生dns發生域名劫持,因此需要手動將URL請求的域名重定向到指定的IP地址,但是由於請求可能是通過NSURLConnection,NSURLSession或者AFNetworking等方式,因此要想統一進行處理,一開始是想通過Method Swizzling去hook cfnetworking底層方法,後來發現其實有個更好的方法–NSURLProtocol。
NSURLProtocol
NSURLProtocol能夠讓你去重新定義蘋果的URL載入系統 (URL Loading System)的行為,URL Loading System裡有許多類用於處理URL請求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,當URL Loading System使用NSURLRequest去獲取資源的時候,它會建立一個NSURLProtocol子類的例項,你不應該直接例項化一個NSURLProtocol,NSURLProtocol看起來像是一個協議,但其實這是一個類,而且必須使用該類的子類,並且需要被註冊。
使用場景
不管你是通過UIWebView, NSURLConnection 或者第三方庫 (AFNetworking, MKNetworkKit等),他們都是基於NSURLConnection或者 NSURLSession實現的,因此你可以通過NSURLProtocol做自定義的操作。
- 重定向網路請求
- 忽略網路請求,使用本地快取
- 自定義網路請求的返回結果
- 一些全域性的網路請求設定
攔截網路請求
子類化NSURLProtocol並註冊
1 2 |
@interface CustomURLProtocol : NSURLProtocol @end |
然後在application:didFinishLaunchingWithOptions:方法中註冊該CustomURLProtocol,一旦註冊完畢後,它就有機會來處理所有交付給URL Loading system的網路請求。
1 2 3 4 5 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //註冊protocol [NSURLProtocol registerClass:[CustomURLProtocol class]]; return YES; } |
實現CustomURLProtocol
註冊好了之後,現在可以開始實現NSURLProtocol的一些方法:
- +canInitWithRequest:
這個方法主要是說明你是否打算處理對應的request,如果不打算處理,返回NO,URL Loading System會使用系統預設的行為去處理;如果打算處理,返回YES,然後你就需要處理該請求的所有東西,包括獲取請求資料並返回給 URL Loading System。網路資料可以簡單的通過NSURLConnection去獲取,而且每個NSURLProtocol物件都有一個NSURLProtocolClient例項,可以通過該client將獲取到的資料返回給URL Loading System。
這裡有個需要注意的地方,想象一下,當你去載入一個URL資源的時候,URL Loading System會詢問CustomURLProtocol是否能處理該請求,你返回YES,然後URL Loading System會建立一個CustomURLProtocol例項然後呼叫NSURLConnection去獲取資料,然而這也會呼叫URL Loading System,而你在+canInitWithRequest:中又總是返回YES,這樣URL Loading System又會建立一個CustomURLProtocol例項導致無限迴圈。我們應該保證每個request只被處理一次,可以通過+setProperty:forKey:inRequest:標示那些已經處理過的request,然後在+canInitWithRequest:中查詢該request是否已經處理過了,如果是則返回NO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { //只處理http和https請求 NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) { //看看是否已經處理過了,防止無限迴圈 if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) { return NO; } return YES; } return NO; } |
- +canonicalRequestForRequest:
通常該方法你可以簡單的直接返回request,但也可以在這裡修改request,比如新增header,修改host等,並返回一個新的request,這是一個抽象方法,子類必須實現。
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 |
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { NSMutableURLRequest *mutableReqeust = [request mutableCopy]; mutableReqeust = [self redirectHostInRequset:mutableReqeust]; return mutableReqeust; } +(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request { if ([request.URL host].length == 0) { return request; } NSString *originUrlString = [request.URL absoluteString]; NSString *originHostString = [request.URL host]; NSRange hostRange = [originUrlString rangeOfString:originHostString]; if (hostRange.location == NSNotFound) { return request; } //定向到bing搜尋主頁 NSString *ip = @"cn.bing.com"; // 替換域名 NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip]; NSURL *url = [NSURL URLWithString:urlString]; request.URL = url; return request; } |
- +requestIsCacheEquivalent:toRequest:
主要判斷兩個request是否相同,如果相同的話可以使用快取資料,通常只需要呼叫父類的實現。
1 2 3 4 |
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } |
- -startLoading -stopLoading
這兩個方法主要是開始和取消相應的request,而且需要標示那些已經處理過的request。
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; //標示改request已經處理過了,防止無限迴圈 [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust]; self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self]; } - (void)stopLoading { [self.connection cancel]; } |
- NSURLConnectionDataDelegate方法
在處理網路請求的時候會呼叫到該代理方法,我們需要將收到的訊息通過client返回給URL Loading System。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (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]; } |
現在你已經可以擷取request並做你想做的事了,這裡有個demo可以參考一下,擷取request並重新定向到新的地址,具體dns解析方法可以參看DNS解析) ,如有不對,歡迎指正,哈~