詳解iOS開發(fā)之NSURLProtocol的那些坑
NSURLProtocol
NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統(tǒng) (URL Loading System)的行為,URL Loading System里有許多類用于處理URL請求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,當(dāng)URL Loading System使用NSURLRequest去獲取資源的時候,它會創(chuàng)建一個NSURLProtocol子類的實例,你不應(yīng)該直接實例化一個NSURLProtocol,NSURLProtocol看起來像是一個協(xié)議,但其實這是一個類,而且必須使用該類的子類,并且需要被注冊。
使用場景
不管你是通過UIWebView, NSURLConnection 或者第三方庫 (AFNetworking, MKNetworkKit等),他們都是基于NSURLConnection或者 NSURLSession實現(xiàn)的,因此你可以通過NSURLProtocol做自定義的操作。
- 重定向網(wǎng)絡(luò)請求
- 忽略網(wǎng)絡(luò)請求,使用本地緩存
- 自定義網(wǎng)絡(luò)請求的返回結(jié)果
- 一些全局的網(wǎng)絡(luò)請求設(shè)置
接觸過iOS系統(tǒng)中URL Loading System都知道,NSURLProtocol是如此地強大,可以攔截應(yīng)用內(nèi)幾乎所有的網(wǎng)絡(luò)請求(除了WKWebView),并可以修改請求頭,返回client任意自定義的數(shù)據(jù)等等,據(jù)說很多做網(wǎng)絡(luò)緩存都是利用這個類的。
那么,首先講解一下NSURLProtocol怎么使用吧。
1. 定義一個NSURLProtocol的子類
在繼承NSURLProtocol中,我們需要實現(xiàn)
+ (BOOL)canInitWithRequest:(NSURLRequest *)request, 定義攔截請求的URL規(guī)則
- (void)startLoading, 對于攔截的請求,系統(tǒng)創(chuàng)建一個NSURLProtocol對象執(zhí)行startLoading方法開始加載請求
- (void)stopLoading,對于攔截的請求,NSURLProtocol對象在停止加載時調(diào)用該方法
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request,可選方法,對于需要修改請求頭的請求在該方法中修改
下面代碼定義了一個專門攔截https請求的NSURLProtocol子類,并通過CFHttpMessageRef重新請求
@interface CFHttpMessageURLProtocol () <NSStreamDelegate> { NSMutableURLRequest *curRequest; NSRunLoop *curRunLoop; NSInputStream *inputStream; } @end @implementation CFHttpMessageURLProtocol /** * 是否攔截處理指定的請求 * * @param request 指定的請求 * * @return 返回YES表示要攔截處理,返回NO表示不攔截處理 */ + (BOOL)canInitWithRequest:(NSURLRequest *)request { /* 防止無限循環(huán),因為一個請求在被攔截處理過程中,也會發(fā)起一個請求,這樣又會走到這里,如果不進行處理,就會造成無限循環(huán) */ if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) { return NO; } NSString *url = request.URL.absoluteString; // 如果url以https開頭,則進行攔截處理,否則不處理 if ([url hasPrefix:@"https"]) { return YES; } return NO; } /** * 如果需要對請求進行重定向,添加指定頭部等操作,可以在該方法中進行 */ + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } /** * 開始加載,在該方法中,加載一個請求 */ - (void)startLoading { NSMutableURLRequest *request = [self.request mutableCopy]; // 表示該請求已經(jīng)被處理,防止無限循環(huán) [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request]; curRequest = request; [self startRequest]; } /** * 取消請求 */ - (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方法是通過復(fù)制原始請求頭,使用CFHttpMessageRef重新發(fā)起請求的,關(guān)于這部分的代碼由于跟本文章內(nèi)容關(guān)系不大,這里就先不放,有興趣的朋友可以參考我的下一篇博客。
2. 在網(wǎng)絡(luò)請求前注冊NSURLProtocol
// 注冊攔截請求的NSURLProtocol [NSURLProtocol registerClass:[CFHttpMessageURLProtocol class]];
對于NSURLSession的請求,注冊NSURLProtocol的方式稍有不同,是通過NSURLSessionConfiguration注冊的
// 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. 請求結(jié)束后注銷NSURLProtocol
[NSURLProtocol unregisterClass:[CFHttpMessageURLProtocol class]];
好了,到這里NSURLProtocol的使用方法大家應(yīng)該有所了解了。下面主要講一下NSURLProtocol在使用過程中可能會遇到的坑,給自己以及需要的朋友留個提醒。
1. 上面一開始就已經(jīng)說了,對于WebView的請求,目前NSURLProtocol還不能攔截WKWebView的請求,只能攔截UIWebview的,但后者好像AppStore已經(jīng)不讓審核通過了(尷尬臉)。
2. NSURLProtocol在攔截NSURLSession的POST請求時不能獲取到Request中的HTTPBody,這個貌似早就國外的論壇上傳開了,但國內(nèi)好像還鮮有人知,據(jù)蘋果官方的解釋是Body是NSData類型,即可能為二進制內(nèi)容,而且還沒有大小限制,所以可能會很大,為了性能考慮,索性就攔截時就不拷貝了(內(nèi)流滿面臉)。為了解決這個問題,我們可以通過把Body數(shù)據(jù)放到Header中,不過Header的大小好像是有限制的,我試過2M是沒有問題,不過超過10M就直接Request timeout了。。。而且當(dāng)Body數(shù)據(jù)為二進制數(shù)據(jù)時這招也沒轍了,因為Header里都是文本數(shù)據(jù),另一種方案就是用一個NSDictionary或NSCache保存沒有請求的Body數(shù)據(jù),用URL為key,最后方法就是別用NSURLSession,老老實實用古老的NSURLConnection算了。。。
3. 使用NSURLProtocol時,在那兩個類方法可以發(fā)送同步網(wǎng)絡(luò)請求,而實例方法,如startLoading則進入死鎖,直至超時,原因是執(zhí)行實例方法所在的線程并沒有啟動runloop,而NSURLConnection這些網(wǎng)絡(luò)請求需要依賴于runloop的,因此這些請求根本發(fā)不出去,所以必須使用異步請求,NSURLConnection/NSURLSession的異步請求的線程保證啟動了runloop。
以上就是我目前發(fā)現(xiàn)的坑,歡迎大家補充,也希望對大家開發(fā)有所幫助哈~所幸的是NSURLProtocol對于大量并發(fā)的請求支持的還不錯,不然就要棄用了~希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
iOS應(yīng)用開發(fā)中SQLite的初步配置指南
這篇文章主要介紹了iOS應(yīng)用開發(fā)中SQLite的初步配置指南,SQLite是一個極輕量級可作嵌入式的數(shù)據(jù)庫,非常適合入門開發(fā)者使用,需要的朋友可以參考下2015-12-12Ios蘋果app應(yīng)用程序開發(fā)者如何獲取IPA簽名證書詳解
這篇文章主要為大家介紹了Ios蘋果app應(yīng)用程序開發(fā)者如何獲取IPA簽名證書詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11iOS Swift創(chuàng)建代理協(xié)議的多種方式示例
這篇文章主要給大家介紹了關(guān)于iOS Swift創(chuàng)建代理協(xié)議的多種方式,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12iOS開發(fā)之時間戳(或date)轉(zhuǎn)字符串的實例代碼
這篇文章主要介紹了iOS開發(fā)之時間戳(或date)轉(zhuǎn)字符串的實例代碼,需要的朋友可以參考下2017-10-10LRecyclerView側(cè)滑iOS阻塞效果不完整的解決辦法
這篇文章主要介紹了LRecyclerView側(cè)滑iOS阻塞效果不完整的解決辦法,非常不錯,具有參考借鑒價值,需要的朋友參考下2016-12-12