注意區分類方法與實例方法,對理解NSProtocol 的原理有幫助。web
啓動時註冊 NSURLProtocol 類的實現類 MyURLProtocol:緩存
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [NSURLProtocol registerClass:MyURLProtocol.class]; return YES; }
UI 界面中,從一個文本框輸入url 發送請求:網絡
#pragma mark - IBAction - (IBAction)buttonGoClicked:(id)sender { if ([self.textField isFirstResponder]) { [self.textField resignFirstResponder]; } [self sendRequest]; } #pragma mark - UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; [self sendRequest]; return YES; } #pragma mark - Private - (void) sendRequest { NSString *text = self.textField.text; if (![text isEqualToString:@""]) { NSURL *url = [NSURL URLWithString:text]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [self.webView loadRequest:request]; } }
以後就是 MyURLPotocol 的部分了:app
MyURLProtocolHandledKey是一個常亮字符串,用來標識一個request是否應該被攔截.ide
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { if ([NSURLProtocol propertyForKey:MyURLProtocolHandledKey inRequest:request]) { NSLog(@"NO"); return NO; } NSLog(@"YES"); return YES; }
若是canInitWithRequest:返回NO, 就表示 當前註冊的MyURLPotocol 不能處理這個請求, MyURLPotocol 就會被繞過去,就像不存在同樣,原來的request 按照它原本的邏輯去加載;url
若是canInitWithRequest: 返回YES,就表示 當前註冊的MyURLPotocol 能夠處理這個請求,接下來會調用 MyURLPotocol 的 canonicalRequestForRequest:方法等,接下來會生成一個MyURLProtocol 的實例,調用實例的startLoading 方法等。spa
startLoading 裏面是咱們攔截下請求後本身的處理邏輯, code
1)首先檢測是否有符合當前request的緩存對象(使用CoreData保存),若是有的話就把緩存對象做爲請求的返回,並顯示調用client ( NSURLProtocolClient類型,和 NSURLConnectionDelegate很是類似)喚起 URL Loading System 的回調。注意緩存規則使用了 不容許緩存 , 由於咱們使用已經CoreData進行緩存了。對象
2)若是沒有緩存對象, 則複製當前request ,並設置 MyURLProtocolHandledKey 標誌,告訴 MyURLPotocol 不攔截這個請求( 由於全部的url 請求都會經過 NSURLProtocol的 canInitWithRequest:方法的檢測),最後經過正常的NSURLConnection 或 NSURLSession 發送請求。事件
NSURLConnection 須要一個 NSURLConnectionDelegate 來接收 事件的回調(響應、數據、成功、失敗等事件),這裏MyURLPotocol 實現了 NSURLConnectionDelegate,因此註冊爲 self。
- (void) startLoading { NSLog(@"startLoading"); CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest]; if (cachedResponse) { NSData *data = cachedResponse.data; NSString *mimeType = cachedResponse.mimeType; NSString *encoding = cachedResponse.encoding; NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:mimeType expectedContentLength:data.length textEncodingName:encoding]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [self.client URLProtocol:self didLoadData:data]; [self.client URLProtocolDidFinishLoading:self]; } else { NSMutableURLRequest *newRequest = [self.request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:MyURLProtocolHandledKey inRequest:newRequest]; self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; } }
NSURLConnectionDelegate 方法的實現:
在這些方法中,仍然要經過 client 對象通知 URL Loading System;
在connectiongDidFinishLoading:方法裏,使用CoreData 保存請求返回的數據。
#pragma mark - NSURLConnectionDelegate - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"=> connection: didReceiveResponse: "); [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.response = response; self.mutableData = [[NSMutableData alloc] init]; } - (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"=> == =====connection: didReceiveData: ======="); [self.client URLProtocol:self didLoadData:data]; [self.mutableData appendData:data]; } - (void) connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; [self saveCachedResponse]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; }
請求的過程如圖所示, 虛線表示 請求被攔截後發現找不到本地緩存,須要經過 URLConnection 發送網絡請求的狀況:
注:一個request 對應一個 NSURLProtocol 實例, 注意 request 和response的對應的關係,