歡迎訪問(wèn)我的博客原文
NSURLProtocol 是什么
NSURLProtocol 是 Foundation 框架中 URL Loading System 的一部分遭贸。它可以讓開(kāi)發(fā)者可以在不修改應(yīng)用內(nèi)原始請(qǐng)求代碼的情況下,去改變 URL 加載的全部細(xì)節(jié)加匈。換句話說(shuō),NSURLProtocol 是一個(gè)被 Apple 默許的中間人攻擊憎兽。
雖然 NSURLProtocol 叫“Protocol”撑蚌,卻不是協(xié)議,而是一個(gè)抽象類(lèi)刽肠。
既然 NSURLProtocol 是一個(gè)抽象類(lèi)溃肪,說(shuō)明它無(wú)法被實(shí)例化免胃,那么它又是如何實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求攔截的?
答案就是通過(guò)子類(lèi)化來(lái)定義新的或是已經(jīng)存在的 URL 加載行為惫撰。如果當(dāng)前的網(wǎng)絡(luò)請(qǐng)求是可以被攔截的羔沙,那么開(kāi)發(fā)者只需要將一個(gè)自定義的 NSURLProtocol 子類(lèi)注冊(cè)到 App 中,在這個(gè)子類(lèi)中就可以攔截到所有請(qǐng)求并進(jìn)行修改厨钻。
那么到底哪些網(wǎng)絡(luò)請(qǐng)求可以被攔截扼雏?
NSURLProtocol 使用場(chǎng)景
前面已經(jīng)說(shuō)了,NSURLProtocol 是 URL Loading System 的一部分夯膀,所以它可以攔截所有基于 URL Loading System 的網(wǎng)絡(luò)請(qǐng)求:
- NSURLSession
- NSURLConnection
- NSURLDownload
- NSURLResponse
- NSHTTPURLResponse
- NSURLRequest
- NSMutableURLRequest
相應(yīng)的诗充,基于它們實(shí)現(xiàn)的第三方網(wǎng)絡(luò)框架 AFNetworking 和 Alamofire 的網(wǎng)絡(luò)請(qǐng)求,也可以被 NSURLProtocol 攔截到诱建。
但早些年基于 CFNetwork 實(shí)現(xiàn)的蝴蜓,比如 ASIHTTPRequest,其網(wǎng)絡(luò)請(qǐng)求就無(wú)法被攔截涂佃。
另外励翼,UIWebView 也是可以被 NSURLProtocol 攔截的,但 WKWebView 不可以辜荠。(因?yàn)?WKWebView 是基于 WebKit汽抚,并不走 C socket。)
因此伯病,在實(shí)際應(yīng)用中造烁,它的功能十分強(qiáng)大,比如:
- 重定向網(wǎng)絡(luò)請(qǐng)求午笛,解決 DNS 域名劫持的問(wèn)題
- 進(jìn)行全局或局部的網(wǎng)絡(luò)請(qǐng)求設(shè)置惭蟋,比如修改請(qǐng)求地址、header 等
- 忽略網(wǎng)絡(luò)請(qǐng)求药磺,使用 H5 離線包或是緩存數(shù)據(jù)等
- 自定義網(wǎng)絡(luò)請(qǐng)求的返回結(jié)果告组,比如過(guò)濾敏感信息
下面來(lái)看一下 NSURLProtocol 的相關(guān)方法。
NSURLProtocol 的相關(guān)方法
創(chuàng)建協(xié)議對(duì)象
// 創(chuàng)建一個(gè) URL 協(xié)議實(shí)例來(lái)處理 request 請(qǐng)求
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
// 創(chuàng)建一個(gè) URL 協(xié)議實(shí)例來(lái)處理 session task 請(qǐng)求
- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
注冊(cè)和注銷(xiāo)協(xié)議類(lèi)
// 嘗試注冊(cè) NSURLProtocol 的子類(lèi)癌佩,使之在 URL 加載系統(tǒng)中可見(jiàn)
+ (BOOL)registerClass:(Class)protocolClass;
// 注銷(xiāo) NSURLProtocol 的指定子類(lèi)
+ (void)unregisterClass:(Class)protocolClass;
確定子類(lèi)是否可以處理請(qǐng)求
子類(lèi)化 NSProtocol 的首要任務(wù)就是告知它木缝,需要控制什么類(lèi)型的網(wǎng)絡(luò)請(qǐng)求。
// 確定協(xié)議子類(lèi)是否可以處理指定的 request 請(qǐng)求围辙,如果返回 YES我碟,請(qǐng)求會(huì)被其控制,返回 NO 則直接跳入下一個(gè) protocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 確定協(xié)議子類(lèi)是否可以處理指定的 task 請(qǐng)求
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task;
獲取和設(shè)置請(qǐng)求屬性
NSURLProtocol 允許開(kāi)發(fā)者去獲取姚建、添加矫俺、刪除 request 對(duì)象的任意元數(shù)據(jù)。這幾個(gè)方法常用來(lái)處理請(qǐng)求無(wú)限循環(huán)的問(wèn)題。
// 在指定的請(qǐng)求中獲取與指定鍵關(guān)聯(lián)的屬性
+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
// 設(shè)置與指定請(qǐng)求中的指定鍵關(guān)聯(lián)的屬性
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
// 刪除與指定請(qǐng)求中的指定鍵關(guān)聯(lián)的屬性
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
提供請(qǐng)求的規(guī)范版本
如果你想要用特定的某個(gè)方式來(lái)修改請(qǐng)求厘托,可以用下面這個(gè)方法友雳。
// 返回指定請(qǐng)求的規(guī)范版本
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
確定請(qǐng)求是否相同
// 判斷兩個(gè)請(qǐng)求是否相同,如果相同可以使用緩存數(shù)據(jù)铅匹,通常只需要調(diào)用父類(lèi)的實(shí)現(xiàn)
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
啟動(dòng)和停止加載
這是子類(lèi)中最重要的兩個(gè)方法沥阱,不同的自定義子類(lèi)在調(diào)用這兩個(gè)方法時(shí)會(huì)傳入不同的內(nèi)容,但共同點(diǎn)都是圍繞 protocol 客戶端進(jìn)行操作伊群。
// 開(kāi)始加載
- (void)startLoading;
// 停止加載
- (void)stopLoading;
獲取協(xié)議屬性
// 獲取協(xié)議接收者的緩存
- (NSCachedURLResponse *)cachedResponse;
// 接受者用來(lái)與 URL 加載系統(tǒng)通信的對(duì)象考杉,每個(gè) NSProtocol 的子類(lèi)實(shí)例都擁有它
- (id<NSURLProtocolClient>)client;
// 接收方的請(qǐng)求
- (NSURLRequest *)request;
// 接收方的任務(wù)
- (NSURLSessionTask *)task;
NSURLProtocol 在實(shí)際應(yīng)用中,主要是完成兩步:攔截 URL 和 URL 轉(zhuǎn)發(fā)舰始。先來(lái)看如何攔截網(wǎng)絡(luò)請(qǐng)求崇棠。
如何利用 NSProtocol 攔截網(wǎng)絡(luò)請(qǐng)求
創(chuàng)建 NSURLProtocol 子類(lèi)
這里創(chuàng)建一個(gè)名為 HTCustomURLProtocol
的子類(lèi)。
@interface HTCustomURLProtocol : NSURLProtocol
@end
注冊(cè) NSURLProtocol 的子類(lèi)
在合適的位置注冊(cè)這個(gè)子類(lèi)丸卷。對(duì)基于 NSURLConnection 或者使用 [NSURLSession sharedSession]
初始化對(duì)象創(chuàng)建的網(wǎng)絡(luò)請(qǐng)求枕稀,調(diào)用 registerClass
方法即可。
[NSURLProtocol registerClass:[NSClassFromString(@"HTCustomURLProtocol") class]];
// 或者
// [NSURLProtocol registerClass:[HTCustomURLProtocol class]];
如果需要全局監(jiān)聽(tīng)谜嫉,可以設(shè)置在 AppDelegate.m
的 didFinishLaunchingWithOptions
方法中萎坷。如果只需要在單個(gè) UIViewController 中使用,記得在合適的時(shí)機(jī)注銷(xiāo)監(jiān)聽(tīng):
[NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]];
如果是基于 NSURLSession 的網(wǎng)絡(luò)請(qǐng)求沐兰,且不是通過(guò) [NSURLSession sharedSession]
方式創(chuàng)建的哆档,就得配置 NSURLSessionConfiguration 對(duì)象的 protocolClasses
屬性。
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"HTCustomURLProtocol") class]];
實(shí)現(xiàn) NSURLProtocol 子類(lèi)
實(shí)現(xiàn)子類(lèi)分為五個(gè)步驟:
注冊(cè) → 攔截 → 轉(zhuǎn)發(fā) → 回調(diào) → 結(jié)束
以攔截 UIWebView 為例住闯,這里需要重寫(xiě)父類(lèi)的這五個(gè)核心方法瓜浸。
// 定義一個(gè)協(xié)議 key
static NSString * const HTCustomURLProtocolHandledKey = @"HTCustomURLProtocolHandledKey";
// 在拓展中定義一個(gè) NSURLConnection 屬性。通過(guò) NSURLSession 也可以攔截比原,這里只是以 NSURLConnection 為例插佛。
@property (nonatomic, strong) NSURLConnection *connection;
// 定義一個(gè)可變的請(qǐng)求返回值,
@property (nonatomic, strong) NSMutableData *responseData;
// 方法 1:在攔截到網(wǎng)絡(luò)請(qǐng)求后會(huì)調(diào)用這一方法量窘,可以再次處理攔截的邏輯雇寇,比如設(shè)置只針對(duì) http 和 https 的請(qǐng)求進(jìn)行處理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
// 只處理 http 和 https 請(qǐng)求
NSString *scheme = [[request URL] scheme];
if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) {
// 看看是否已經(jīng)處理過(guò)了蚌铜,防止無(wú)限循環(huán)
if ([NSURLProtocol propertyForKey:HTCustomURLProtocolHandledKey inRequest:request]) {
return NO;
}
// 如果還需要截取 DNS 解析請(qǐng)求中的鏈接锨侯,可以繼續(xù)加判斷,是否為攔截域名請(qǐng)求的鏈接厘线,如果是返回 NO
return YES;
}
return NO;
}
// 方法 2:【關(guān)鍵方法】可以在此對(duì) request 進(jìn)行處理识腿,比如修改地址出革、提取請(qǐng)求信息造壮、設(shè)置請(qǐng)求頭等。
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
// 可以打印出所有的請(qǐng)求鏈接包括 CSS 和 Ajax 請(qǐng)求等
NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
return mutableRequest;
}
// 方法 3:【關(guān)鍵方法】在這里設(shè)置網(wǎng)絡(luò)代理,重新創(chuàng)建一個(gè)對(duì)象將處理過(guò)的 request 轉(zhuǎn)發(fā)出去耳璧。這里對(duì)應(yīng)的回調(diào)方法對(duì)應(yīng) <NSURLProtocolClient> 協(xié)議方法
- (void)startLoading {
// 可以修改 request 請(qǐng)求
NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
// 打 tag成箫,防止遞歸調(diào)用
[NSURLProtocol setProperty:@YES forKey:HTCustomURLProtocolHandledKey inRequest:mutableRequest];
// 也可以在這里檢查緩存
// 將 request 轉(zhuǎn)發(fā),對(duì)于 NSURLConnection 來(lái)說(shuō)旨枯,就是創(chuàng)建一個(gè) NSURLConnection 對(duì)象蹬昌;對(duì)于 NSURLSession 來(lái)說(shuō),就是發(fā)起一個(gè) NSURLSessionTask攀隔。
self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}
// 方法 4:主要判斷兩個(gè) request 是否相同皂贩,如果相同的話可以使用緩存數(shù)據(jù),通常只需要調(diào)用父類(lèi)的實(shí)現(xiàn)昆汹。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
// 方法 5:處理結(jié)束后停止相應(yīng)請(qǐng)求明刷,清空 connection 或 session
- (void)stopLoading {
if (self.connection != nil) {
[self.connection cancel];
self.connection = nil;
}
}
// 按照在上面的方法中做的自定義需求,看情況對(duì)轉(zhuǎn)發(fā)出來(lái)的請(qǐng)求在恰當(dāng)?shù)臅r(shí)機(jī)進(jìn)行回調(diào)處理满粗。
#pragma mark- NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
#pragma mark - NSURLConnectionDataDelegate
// 當(dāng)接收到服務(wù)器的響應(yīng)(連通了服務(wù)器)時(shí)會(huì)調(diào)用
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.responseData = [[NSMutableData alloc] init];
// 可以處理不同的 statusCode 場(chǎng)景
// NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
// 可以設(shè)置 Cookie
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
// 接收到服務(wù)器的數(shù)據(jù)時(shí)會(huì)調(diào)用辈末,可能會(huì)被調(diào)用多次,每次只傳遞部分?jǐn)?shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
[self.client URLProtocol:self didLoadData:data];
}
// 服務(wù)器的數(shù)據(jù)加載完畢后調(diào)用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
// 請(qǐng)求錯(cuò)誤(失斢辰浴)的時(shí)候調(diào)用挤聘,比如出現(xiàn)請(qǐng)求超時(shí)、斷網(wǎng)捅彻,一般指客戶端錯(cuò)誤
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
上面用到的一些 NSURLProtocolClient 方法:
@protocol NSURLProtocolClient <NSObject>
// 請(qǐng)求重定向
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
// 響應(yīng)緩存是否合法
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
// 剛接收到 response 信息
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
// 數(shù)據(jù)加載成功
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
// 數(shù)據(jù)完成加載
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
// 數(shù)據(jù)加載失敗
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
// 為指定的請(qǐng)求啟動(dòng)驗(yàn)證
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
// 為指定的請(qǐng)求取消驗(yàn)證
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
@end
補(bǔ)充內(nèi)容
使用 NSURLSession 時(shí)的注意事項(xiàng)
如果在 NSURLProtocol 中使用 NSURLSession组去,需要注意:
- 攔截到的 request 請(qǐng)求的 HTTPBody 為 nil,但可以借助 HTTPBodyStream 來(lái)獲取 body步淹;
- 如果要用
registerClass
注冊(cè)添怔,只能通過(guò)[NSURLSession sharedSession]
的方式創(chuàng)建網(wǎng)絡(luò)請(qǐng)求。
注冊(cè)多個(gè) NSURLProtocol 子類(lèi)
當(dāng)有多個(gè)自定義 NSURLProtocol 子類(lèi)注冊(cè)到系統(tǒng)中的話贤旷,會(huì)按照他們注冊(cè)的反向順序依次調(diào)用 URL 加載流程广料,也就是最后注冊(cè)的 NSURLProtocol 會(huì)被優(yōu)先判斷。
對(duì)于通過(guò)配置 NSURLSessionConfiguration 對(duì)象的 protocolClasses
屬性來(lái)注冊(cè)的情況幼驶,protocolClasses
數(shù)組中只有第一個(gè) NSURLProtocol 會(huì)起作用艾杏,后續(xù)的 NSURLProtocol 就無(wú)法攔截到了。
所以 OHHTTPStubs 在注冊(cè) NSURLProtocol 子類(lèi)的時(shí)候是這樣處理的:
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
// Runtime check to make sure the API is available on this version
if ([sessionConfig respondsToSelector:@selector(protocolClasses)]
&& [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
{
NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
Class protoCls = HTTPStubsProtocol.class;
if (enable && ![urlProtocolClasses containsObject:protoCls])
{
// 將自己的 NSURLProtocol 插入到 protocolClasses 的第一個(gè)盅藻,進(jìn)行攔截
[urlProtocolClasses insertObject:protoCls atIndex:0];
}
else if (!enable && [urlProtocolClasses containsObject:protoCls])
{
// 攔截完成后移除
[urlProtocolClasses removeObject:protoCls];
}
sessionConfig.protocolClasses = urlProtocolClasses;
}
else
{
NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
@"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
@"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
}
}
如何攔截 WKWebView
雖然 NSURLProtocol 無(wú)法直接攔截 WKWebView购桑,但其實(shí)還是有解決方案的。就是使用 WKBrowsingContextController
和 registerSchemeForCustomProtocol
氏淑。
// 注冊(cè) scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
// 通過(guò) http 和 https 的請(qǐng)求勃蜘,同理可通過(guò)其他的 Scheme 但是要滿足 URL Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}
但由于這涉及到了私有方法,直接引用無(wú)法過(guò)蘋(píng)果的機(jī)審假残,所以使用的時(shí)候需要對(duì)字符串做下處理缭贡,比如對(duì)方法名進(jìn)行算法加密處理等炉擅,實(shí)測(cè)也是可以通過(guò)審核的。
總之阳惹,NSURLProtocol 非常強(qiáng)大谍失,無(wú)論是優(yōu)化 App 的性能,還是拓展功能莹汤,都具有很強(qiáng)的可塑空間快鱼,但在使用的同時(shí),又要多關(guān)注它帶來(lái)的問(wèn)題纲岭。盡管它在很多框架或者知名項(xiàng)目中都已經(jīng)得以應(yīng)用抹竹,其奧義依然值得開(kāi)發(fā)者們?nèi)ド钊胙芯俊?/p>