先說下URL Loading System

如圖所示,URL Loading System是iOS一系列網(wǎng)絡(luò)請求類的集合,包括已經(jīng)過期不用的NSConnection和現(xiàn)在流行的NSURLSession,還包括一些請求認(rèn)證的類,一個sessionConfig的類,還有關(guān)于處理請求緩存的類等,當(dāng)然還包括我們要說的這個NSURLProtocol類。
對,我沒說錯,NSURLPtotocol類并不是一個protocol,他其實就是一個類,而且是一個“虛基類”-虛擬的父類吧。
URL Loading System可以發(fā)出的請求種類有ftp://,http://,https://,file://,data:// 請求。
NSURLProtocol的作用
NSURLProtocol可以攔截監(jiān)聽每一個URL Loading System中發(fā)出request請求,記住是URL Loading System中那些類發(fā)出的請求,也支持AFNetwoking,UIWebView發(fā)出的request。如果不是這些類發(fā)出的請求,NSURLProtocol就沒辦法攔截和監(jiān)聽了。
- 忽略網(wǎng)絡(luò)請求使用本地緩存
- 重定向網(wǎng)絡(luò)請求
- 改變request的請求頭
NSURLProtocol的使用
因為NSURLProtocol是一個虛基類,所以不能直接使用它,要想使用它就必須自定義一個類成為他的子類,然后實現(xiàn)他里面的必須實現(xiàn)的一些方法,那么我們還要告訴系統(tǒng):“喂,你發(fā)出的request,要讓我的子類XXX類過一遍啊!”所以NSURLProtocol有一個register方法告訴系統(tǒng)那個子類要起作用。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[TFURLProtocol class]];
return YES;
}
相對應(yīng)的也有unregistClass方法,不讓某個子類起作用,這個起作用的時候并不是一定要在appDelegate中,你想要他在什么時候起作用,某個請求之前注冊他就行,相應(yīng)的不想他起作用就unregist他就行了。
子類必須實現(xiàn)的一些方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
每次有一個請求的時候都會調(diào)用這個方法,在這個方法里面判斷這個請求是否需要被處理攔截,如果返回YES就代表這個request需要被處理,反之就是不需要被處理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
return YES;
}
return NO;
}
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
這個方法就是返回規(guī)范的request,一般使用就是直接返回request,不做任何處理的
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
- (void)startLoading
這個方法作用很大,把當(dāng)前請求的request攔截下來以后,在這個方法里面對這個request做各種處理,比如添加請求頭,重定向網(wǎng)絡(luò),使用自定義的緩存等。作用非常之大。下面就是一個重定向的例子。
/**
* 開始請求
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
//把訪問百度的request改為訪問Google了
request.URL = [NSURL URLWithString:@"http://www.google.com"];
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
//使用NSURLSession繼續(xù)把重定向的request發(fā)送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
[task resume];
}
- (void)stopLoading
相應(yīng)的還有一個停止請求的方法,也是要實現(xiàn)的。
死循環(huán)的坑
有沒有看到這兩句代碼?
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
這兩句是為了防止死循環(huán)的,也是NSURLProtocol里必須寫的方法。試想一下當(dāng)我在startLoading的時候還會繼續(xù)發(fā)出這個request,那么這個時候還是會攔截到這個request,然后進(jìn)行處理,然后再次在startLoading中發(fā)送出去,然后繼續(xù)攔截。。。。。。。。
所以在我們startLoading里面,我們對這個request進(jìn)行標(biāo)記,標(biāo)記他已經(jīng)被處理過了,然后在canInitWithRequest方法中根據(jù)這個標(biāo)記拿到這個request,如果被標(biāo)記了,就不再次進(jìn)行處理了,如果沒有標(biāo)記過就要進(jìn)行處理,這就很好的解決了死循環(huán)問題。
NSURLProtocolClient
如果我們使用UIWebView發(fā)送一個request,攔截以后當(dāng)我們使用NSURLSession發(fā)出了request,那么這個request的response是無法回到這個UIWebView的,因為可以理解成不是同一個地方發(fā)出的request,這個response只能有session來處理,那我們怎么才能讓這個response回到剛開始的UIWebView呢?
NSURLProtocolClient就可以看做是URL Loading System,我們把response告訴client,也就是URL Loading System,讓他來繼續(xù)處理這個response,因為一切都是基于URL Loading System發(fā)生的,所以把response交給他,他會自動處理這個response回到webView。
每一個NSURLProtocol的子類都有一個client對象來處理請求得到的response。其實下面這些寫法都是差不多固定的。
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))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];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
completionHandler(proposedResponse);
}
總結(jié)
NSURLProtocol的一些坑
- 死循環(huán)
- 調(diào)試惡心。因為打開一個頁面,里面的每一個請求包括網(wǎng)頁圖片等都會去走一遍子類中請求處理的判斷方法,導(dǎo)致很多想調(diào)試的request找不到。
- WKWebView不起作用,因為WKWebView走得是WebKit內(nèi)核,不走蘋果這一套邏輯,目前貌似還沒有有效的解決方法。
注意點
可以注冊多個NSURLProtocol的子類,注冊多個NSURLProtocol子類會逆序去執(zhí)行,也就是先注冊的子類后執(zhí)行。
常見用法總結(jié)
- 重定向網(wǎng)絡(luò)請求(已經(jīng)舉過例子了)
- 改變request的請求頭
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
//給請求頭添加一個請求體
NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];
[headers setObject:@"ttf" forKey:@"i am ttf"];
request.allHTTPHeaderFields = headers;
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
.....然后使用NSURLSession發(fā)送request
}
- 忽略網(wǎng)絡(luò)請求使用本地緩存
首先自定一個URLResponse類,把資源轉(zhuǎn)化為這個自定義類落地持久化,然后把這個類轉(zhuǎn)換成URL Loading System可以接受的NSURLResponse類,發(fā)送給client,其實主要就是startLoading里面。
- (void) startLoading {
//1\. 獲取緩存的response
CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest];
//2\. 判斷緩存response是否存在
if (cachedResponse) {
NSData *data = cachedResponse.data;
NSString *mimeType = cachedResponse.mimeType;
NSString *encoding = cachedResponse.encoding;
//構(gòu)造一個新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
MIMEType:mimeType
expectedContentLength:data.length
textEncodingName:encoding];
//將新的response作為request對應(yīng)的response
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
//設(shè)置request對應(yīng)的 響應(yīng)數(shù)據(jù) response data
[self.client URLProtocol:self didLoadData:data];
//標(biāo)記請求結(jié)束
[self.client URLProtocolDidFinishLoading:self];
} else {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:MyURLProtocolHandledKey
inRequest:newRequest];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [session dataTaskWithRequest:newRequest];
[task resume];
}
}
另外也可以參考一下“OHHTTPStubs的實現(xiàn)方式”,核心就是使用的NSURLProtocol。