詳解iOS開發(fā)之NSURLProtocol的那些坑
NSURLProtocol
NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統(tǒng) (URL Loading System)的行為,URL Loading System里有許多類用于處理URL請(qǐng)求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,當(dāng)URL Loading System使用NSURLRequest去獲取資源的時(shí)候,它會(huì)創(chuàng)建一個(gè)NSURLProtocol子類的實(shí)例,你不應(yīng)該直接實(shí)例化一個(gè)NSURLProtocol,NSURLProtocol看起來(lái)像是一個(gè)協(xié)議,但其實(shí)這是一個(gè)類,而且必須使用該類的子類,并且需要被注冊(cè)。
使用場(chǎng)景
不管你是通過(guò)UIWebView, NSURLConnection 或者第三方庫(kù) (AFNetworking, MKNetworkKit等),他們都是基于NSURLConnection或者 NSURLSession實(shí)現(xiàn)的,因此你可以通過(guò)NSURLProtocol做自定義的操作。
- 重定向網(wǎng)絡(luò)請(qǐng)求
- 忽略網(wǎng)絡(luò)請(qǐng)求,使用本地緩存
- 自定義網(wǎng)絡(luò)請(qǐng)求的返回結(jié)果
- 一些全局的網(wǎng)絡(luò)請(qǐng)求設(shè)置
接觸過(guò)iOS系統(tǒng)中URL Loading System都知道,NSURLProtocol是如此地強(qiáng)大,可以攔截應(yīng)用內(nèi)幾乎所有的網(wǎng)絡(luò)請(qǐng)求(除了WKWebView),并可以修改請(qǐng)求頭,返回client任意自定義的數(shù)據(jù)等等,據(jù)說(shuō)很多做網(wǎng)絡(luò)緩存都是利用這個(gè)類的。
那么,首先講解一下NSURLProtocol怎么使用吧。
1. 定義一個(gè)NSURLProtocol的子類
在繼承NSURLProtocol中,我們需要實(shí)現(xiàn)
+ (BOOL)canInitWithRequest:(NSURLRequest *)request, 定義攔截請(qǐng)求的URL規(guī)則
- (void)startLoading, 對(duì)于攔截的請(qǐng)求,系統(tǒng)創(chuàng)建一個(gè)NSURLProtocol對(duì)象執(zhí)行startLoading方法開始加載請(qǐng)求
- (void)stopLoading,對(duì)于攔截的請(qǐng)求,NSURLProtocol對(duì)象在停止加載時(shí)調(diào)用該方法
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request,可選方法,對(duì)于需要修改請(qǐng)求頭的請(qǐng)求在該方法中修改
下面代碼定義了一個(gè)專門攔截https請(qǐng)求的NSURLProtocol子類,并通過(guò)CFHttpMessageRef重新請(qǐng)求
@interface CFHttpMessageURLProtocol () <NSStreamDelegate> {
NSMutableURLRequest *curRequest;
NSRunLoop *curRunLoop;
NSInputStream *inputStream;
}
@end
@implementation CFHttpMessageURLProtocol
/**
* 是否攔截處理指定的請(qǐng)求
*
* @param request 指定的請(qǐng)求
*
* @return 返回YES表示要攔截處理,返回NO表示不攔截處理
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
/* 防止無(wú)限循環(huán),因?yàn)橐粋€(gè)請(qǐng)求在被攔截處理過(guò)程中,也會(huì)發(fā)起一個(gè)請(qǐng)求,這樣又會(huì)走到這里,如果不進(jìn)行處理,就會(huì)造成無(wú)限循環(huán) */
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
NSString *url = request.URL.absoluteString;
// 如果url以https開頭,則進(jìn)行攔截處理,否則不處理
if ([url hasPrefix:@"https"]) {
return YES;
}
return NO;
}
/**
* 如果需要對(duì)請(qǐng)求進(jìn)行重定向,添加指定頭部等操作,可以在該方法中進(jìn)行
*/
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
/**
* 開始加載,在該方法中,加載一個(gè)請(qǐng)求
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
// 表示該請(qǐng)求已經(jīng)被處理,防止無(wú)限循環(huán)
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
curRequest = request;
[self startRequest];
}
/**
* 取消請(qǐng)求
*/
- (void)stopLoading {
if (inputStream.streamStatus == NSStreamStatusOpen) {
[inputStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream setDelegate:nil];
[inputStream close];
}
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]];
}
以上代碼中的startRequest方法是通過(guò)復(fù)制原始請(qǐng)求頭,使用CFHttpMessageRef重新發(fā)起請(qǐng)求的,關(guān)于這部分的代碼由于跟本文章內(nèi)容關(guān)系不大,這里就先不放,有興趣的朋友可以參考我的下一篇博客。
2. 在網(wǎng)絡(luò)請(qǐng)求前注冊(cè)NSURLProtocol
// 注冊(cè)攔截請(qǐng)求的NSURLProtocol [NSURLProtocol registerClass:[CFHttpMessageURLProtocol class]];
對(duì)于NSURLSession的請(qǐng)求,注冊(cè)NSURLProtocol的方式稍有不同,是通過(guò)NSURLSessionConfiguration注冊(cè)的
// NSURLSession例子 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSArray *protocolArray = @[ [CFHttpMessageURLProtocol class] ]; configuration.protocolClasses = protocolArray; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionTask *task = [session dataTaskWithRequest:_request]; [task resume];
3. 請(qǐng)求結(jié)束后注銷NSURLProtocol
[NSURLProtocol unregisterClass:[CFHttpMessageURLProtocol class]];
好了,到這里NSURLProtocol的使用方法大家應(yīng)該有所了解了。下面主要講一下NSURLProtocol在使用過(guò)程中可能會(huì)遇到的坑,給自己以及需要的朋友留個(gè)提醒。
1. 上面一開始就已經(jīng)說(shuō)了,對(duì)于WebView的請(qǐng)求,目前NSURLProtocol還不能攔截WKWebView的請(qǐng)求,只能攔截UIWebview的,但后者好像AppStore已經(jīng)不讓審核通過(guò)了(尷尬臉)。
2. NSURLProtocol在攔截NSURLSession的POST請(qǐng)求時(shí)不能獲取到Request中的HTTPBody,這個(gè)貌似早就國(guó)外的論壇上傳開了,但國(guó)內(nèi)好像還鮮有人知,據(jù)蘋果官方的解釋是Body是NSData類型,即可能為二進(jìn)制內(nèi)容,而且還沒有大小限制,所以可能會(huì)很大,為了性能考慮,索性就攔截時(shí)就不拷貝了(內(nèi)流滿面臉)。為了解決這個(gè)問(wèn)題,我們可以通過(guò)把Body數(shù)據(jù)放到Header中,不過(guò)Header的大小好像是有限制的,我試過(guò)2M是沒有問(wèn)題,不過(guò)超過(guò)10M就直接Request timeout了。。。而且當(dāng)Body數(shù)據(jù)為二進(jìn)制數(shù)據(jù)時(shí)這招也沒轍了,因?yàn)镠eader里都是文本數(shù)據(jù),另一種方案就是用一個(gè)NSDictionary或NSCache保存沒有請(qǐng)求的Body數(shù)據(jù),用URL為key,最后方法就是別用NSURLSession,老老實(shí)實(shí)用古老的NSURLConnection算了。。。
3. 使用NSURLProtocol時(shí),在那兩個(gè)類方法可以發(fā)送同步網(wǎng)絡(luò)請(qǐng)求,而實(shí)例方法,如startLoading則進(jìn)入死鎖,直至超時(shí),原因是執(zhí)行實(shí)例方法所在的線程并沒有啟動(dòng)runloop,而NSURLConnection這些網(wǎng)絡(luò)請(qǐng)求需要依賴于runloop的,因此這些請(qǐng)求根本發(fā)不出去,所以必須使用異步請(qǐng)求,NSURLConnection/NSURLSession的異步請(qǐng)求的線程保證啟動(dòng)了runloop。
以上就是我目前發(fā)現(xiàn)的坑,歡迎大家補(bǔ)充,也希望對(duì)大家開發(fā)有所幫助哈~所幸的是NSURLProtocol對(duì)于大量并發(fā)的請(qǐng)求支持的還不錯(cuò),不然就要棄用了~希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
iOS應(yīng)用開發(fā)中圖片的拉伸問(wèn)題解決方案
這篇文章主要介紹了iOS應(yīng)用開發(fā)中圖片的拉伸問(wèn)題解決方案,有時(shí)圖片的拉伸只需要拉伸中間部分而不拉伸兩端,這是本文所關(guān)注的問(wèn)題,需要的朋友可以參考下2016-02-02
iOS UITableView 拖動(dòng)排序?qū)崿F(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了iOS UITableView 拖動(dòng)排序?qū)崿F(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09
iOS應(yīng)用開發(fā)中視圖控件UIWindow的基本使用教程
這篇文章主要介紹了iOS應(yīng)用開發(fā)中視圖控件UIWindow的基本使用教程,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2016-02-02
解決iOS7上UITextField限制字?jǐn)?shù)輸入導(dǎo)致崩潰問(wèn)題的方法
這篇文章主要為大家分享了解決iOS7上UITextField限制字?jǐn)?shù)輸入導(dǎo)致崩潰問(wèn)題的方法,感興趣的小伙伴們可以參考一下2016-03-03
C++ 中exit(),_exit(),return,abort()函數(shù)的區(qū)別
這篇文章主要介紹了C++ 中exit(),_exit(),return,abort()函數(shù)的區(qū)別的相關(guān)資料,需要的朋友可以參考下2016-12-12
iOS實(shí)現(xiàn)動(dòng)態(tài)的開屏廣告示例代碼
啟動(dòng)圖是在iOS開發(fā)過(guò)程中必不可少的一個(gè)部分,很多app在啟動(dòng)圖之后會(huì)有一張自定義的開屏廣告圖,但是有的時(shí)候需要讓啟動(dòng)圖看起來(lái)就是一個(gè)廣告,而且還要這個(gè)廣告里面會(huì)動(dòng),iOS的啟動(dòng)圖只能是靜態(tài)的,而且固定,為了實(shí)現(xiàn)看起來(lái)的動(dòng)畫效果,只能進(jìn)行偽造了。下面來(lái)一起看看2016-09-09
iOS中containsString和rangeOfString的區(qū)別小結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于iOS中containsString和rangeOfString的一些區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01
iOS開發(fā)之時(shí)間戳(或date)轉(zhuǎn)字符串的實(shí)例代碼
這篇文章主要介紹了iOS開發(fā)之時(shí)間戳(或date)轉(zhuǎn)字符串的實(shí)例代碼,需要的朋友可以參考下2017-10-10

