前言
??因為DNS
發生域名劫持,所以需要手動將URL請求的域名重定向到指定的IP地址,但是由于請求可能是通過NSURLConnection
,NSURLSession
或者AFNetworking
等方式,因此要想統一進行處理,可以采用NSURLProtocol
。
??NSURLProtocol
是蘋果為我們提供的 URL Loading System
的一部分,能夠讓你去重新定義蘋果的URL加載系統 (URL Loading System
)的行為,
URL Loading System
里有許多類用于處理URL
請求,比如NSURL
,NSURLRequest
,NSURLConnection
和NSURLSession
等,當URL Loading System
使用NSURLRequest
去獲取資源的時候,它會創建一個NSURLProtocol
子類的實例,NSURLProtocol
看起來像是一個協議,但其實這是一個類,你不能直接實例化一個NSURLProtocol
,而是需要寫一個繼承自 NSURLProtocol
的子類,并通過- registerClass:
方法注冊我們的協議類,然后 URL
加載系統就會在請求發出時使用我們創建的協議對象對該請求進行處理。
用一句話解釋
NSURLProtocol
:就是一個蘋果允許的中間人攻擊。
NSURLProtocol
可以劫持系統所有基于C socket
的網絡請求。
注意:WKWebView
基于Webkit
,并不走底層的C socket
,所以NSURLProtocol
攔截不了WKWebView中
的請求。
使用場景
??不管你是通過UIWebView
, NSURLConnection
或者第三方庫 (AFNetworking
, Alamofire
等),他們都是基于NSURLConnection
或者 NSURLSession
實現的,因此你可以通過NSURLProtocol
做自定義的操作。
通過 NSURLProtocol
可以比較簡單地就能實現:
- 重定向網絡請求(可以解決電信的
DNS
域名劫持問題)- 忽略網絡請求,使用本地緩存
- 自定義網絡請求的返回結果
Response
- 攔截圖片加載請求,轉為從本地文件加載
- 一些全局的網絡請求設置
- 快速進行測試環境的切換
- 過濾掉一些非法請求
- 網絡的緩存處理(H5離線包 和 網絡圖片緩存)
- 可以攔截
UIWebView
,基于系統的NSURLConnection
或者NSURLSession
進行封裝的網絡請求。目前WKWebView
無法被NSURLProtocol
攔截。- 當有多個自定義
NSURLProtocol
注冊到系統中的話,會按照他們注冊的反向順序依次調用URL加載流程。當其中有一個NSURLProtocol
攔截到請求的話,后續的NSURLProtocol
就無法攔截到該請求。
具體步驟為:
使用NSURLProtocol
的主要可以分為5個步驟:
注冊—>攔截—>轉發—>回調—>結束
即:
注冊NSURLProtocol
子類 -> 使用NSURLProtocol
子類攔截請求 -> 使用NSURLSession
重新發起請求 -> 將NSURLSession
請求的響應內容返回 -> 結束
使用方法
1. 子類化:
由于 NSURLProtocol
是一個抽象類,所以使用的時候必須定義一個它的子類:
#import <Foundation/Foundation.h>
@interface CustomURLProtocol : NSURLProtocol
@end
2. 注冊:
??對于基于NSURLConnection
或者使用[NSURLSession sharedSession]
初始化對象創建的網絡請求,調用registerClass
方法即可。
//注冊protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
??對于基于NSURLSession
的網絡請求,如下所示需要通過配置sessionWithConfiguration:delegate:delegateQueue:
初始化對象的,需要配置對象的protocolClasses
屬性,這個在下面會有詳細的介紹。
// NSURLSession例子
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [CustomURLProtocol class]];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:_request];
[task resume];
??一經注冊之后,所有交給URL Loading system
的網絡請求都會被攔截,所以當不需要攔截的時候,要進行注銷
[NSURLProtocol unregisterClass:[CustomURLProtocol class]];
3. 抽象對象必須實現的方法(攔截)
注冊成功之后,就需要我們的子類去實現抽象方法:
//所有注冊此Protocol的請求都會經過這個方法的判斷
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
//可選方法,對需要攔截的請求進行自定的處理
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
//主要是用來判斷兩個request是否相同,這個方法基本不常用
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
//初始化protocol實例,所有來源的請求都以NSURLRequest形式接收
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client;
/**
開始請求
在這里需要我們手動的把請求發出去,可以使用原生的NSURLSessionDataTask,也可以使用的第三方網絡庫
同時設置"NSURLSessionDataDelegate"協議,接收Server端的響應
*/
- (void)startLoading;
//請求被停止
- (void)stopLoading;
詳細說明:
-
canInitWithRequest
??該方法會拿到reques
t的對象,我們可以通過該方法的返回值來篩選request
是否需要被NSURLProtocol
做攔截處理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString * scheme = [[request.URL scheme] lowercaseString];
if ([scheme isEqual:@"http"]) {
return YES;
}
//看看是否已經處理過了,防止無限循環 根據業務來截取
if ([NSURLProtocol propertyForKey: URLProtocolHandledKey inRequest:request]) {
return NO;
}
return NO;
}
URLProtocolHandledKey
是:
static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";
上面我們就只會攔截http
的請求。
-
canonicalRequestForRequest:
??這個方法用來統一處理請求request
對象的,可以修改頭信息,或者重定向。沒有特殊需要,則直接return request
。
??如果要在這里做重定向以及頭信息的時候注意檢查是否已經添加,因為這個方法可能被調用多次,也可以在后面的方法中做。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
-
requestIsCacheEquivalent:toRequest:
主要判斷兩個request
是否相同,如果相同的話可以使用緩存數據,通常只需要調用父類的實現。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
4. 轉發
??在攔截到網絡請求,并且對網絡請求進行定制處理以后。我們需要將網絡請求重新發送出去,就可以初始化一個NSURLProtocol
對象了:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}
該方法會創建一個NSURLProtocol
實例,在這里直接調用super
的指定構造器方法,實例化一個對象。
-
startLoading
??接下來就是轉發的核心方法startLoading
。在該方法中,把當前請求的request
攔截下來以后,可以在這里修改請求信息,重定向網絡,DNS
解析,使用自定義的緩存等。至于發送的形式,可以是基于NSURLConnection
,NSURLSession
甚至AFNetworking
等網絡庫。對于NSURLConnection
來說,就是創建一個NSURLConnection
,對于NSURLSession
,就是發起一個NSURLSessionTask
。一般下載前需要設置該請求正在進行下載,防止多次下載的情況發生。
重點:需要標記已經處理過的request
下面就是一個重定向的例子:
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//標示該request已經處理過了,防止無限循環
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//使用NSURLSession繼續把request發送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
//定義全局的NSURLSession對象用于stop請求使用
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
[task resume];
}
5. 回調
??既是面向切面的編程,就不能影響到原來網絡請求的邏輯。所以上一步將網絡請求轉發出去以后,當收到網絡請求的返回,還需要再將返回值返回給原來發送網絡請求的地方。
主要需要調用到
//將新的response作為request對應的response
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
//設置request對應的 響應數據 response data
[self.client URLProtocol:self didLoadData:data];
//標記請求結束
[self.client URLProtocolDidFinishLoading:self];
所以上面的startLoading
的完整版本應該是
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//標示該request已經處理過了,防止無限循環
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
//這個enableDebug隨便根據自己的需求了,可以直接攔截到數據返回本地的模擬數據,進行測試
BOOL enableDebug = NO;
if (enableDebug) {
NSString *str = @"測試數據";
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
}
else {
//使用NSURLSession繼續把request發送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
[task resume];
}
}
上面采用NSURLSession
發送的網絡請求,所以實現NSURLSessionDelegate
代理方法進行回調,我們可以做相應的處理,先看看NSURLSessionDelegate
的代理方法:
//接收到返回信息時(還未開始下載), 執行的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
//接收到服務器返回的數據 調用多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;
//請求結束或者是失敗的時候調用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error;
一般默認使用方式為:
- (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
{
// 打印返回數據
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr) {
NSLog(@"***截取數據***: %@", dataStr);
}
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
6. 結束
-
stopLoading
??在一個網絡請求完全結束以后,NSURLProtocol
回調用到。在該方法里,我們完成在結束網絡請求的操作,以NSURLSession
為例:
- (void)stopLoading {
[self.session invalidateAndCancel];
self.session = nil;
}
注意點:
1. 如果startLoading中網絡請求采用的是NSURLConnection,例如:
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//做下標記,防止遞歸調用
[NSURLProtocol setProperty:@YES forKey:hasInitKey inRequest:mutableReqeust];
//這個enableDebug隨便根據自己的需求了,可以直接攔截到數據返回本地的模擬數據,進行測試
BOOL enableDebug = NO;
if (enableDebug) {
NSString *str = @"測試數據";
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
MIMEType:@"text/plain"
expectedContentLength:data.length
textEncodingName:nil];
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
}
else {
//采用了NSURLConnection方式請求
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
}
停止方法為
- (void)stopLoading
{
[self.connection cancel];
self.connection = nil ;
}
那么相應的實現的代理方法為NSURLConnectionDataDelegate
//接收到Response信息
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
//接收到服務器的數據(可能調用多次)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 打印返回數據
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr) {
NSLog(@"***截取數據***: %@", dataStr);
}
[self.client URLProtocol:self didLoadData:data];
}
//結束
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
//出錯
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
2. 攔截AFNetworking
??目前為止,我們上面的代碼已經能夠監控到絕大部分的網絡請求,但是呢,有一個卻是特殊的,比如AFNetworking
請求。
??因為AFNetworking
網絡請求的NSURLSession
實例方法都是通過
sessionWithConfiguration:delegate:delegateQueue:
方法獲得的,我們是不能監聽到的,
然而我們通過[NSURLSession sharedSession]
生成session
就可以攔截到請求,原因就出在NSURLSessionConfiguration
上,我們進到NSURLSessionConfiguration
里面看一下,他有一個屬性:
@property (nullable, copy) NSArray<Class> *protocolClasses;
??我們能夠看出,這是一個NSURLProtocol
數組,上面我們提到了,我們監控網絡是通過注冊NSURLProtocol
來進行網絡監控的,但是通過sessionWithConfiguration:delegate:delegateQueue:
得到的session
,他的configuration
中已經有一個NSURLProtocol
,所以他不會走我們的protocol
來,怎么解決這個問題呢? 其實很簡單,我們將NSURLSessionConfiguration
的屬性protocolClasses
的get
方法hook
掉,通過返回我們自己的protocol
,這樣,我們就能夠監控到通過sessionWithConfiguration:delegate:delegateQueue:
得到的session
的網絡請求。
??所以對于AFNetworking
中網絡請求初始化方法可以修改為:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
//指定其protocolClasses
configuration.protocolClasses = @[[CustomURLProtocol class]];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:configuration];
//不采用manager初始化
//AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http://www.baidu.com" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
也可以通過runtime
來面向切面編程
#import <Foundation/Foundation.h>
@interface FFSessionConfiguration : NSObject
//是否交換方法
@property (nonatomic,assign) BOOL isExchanged;
+ (FFSessionConfiguration *)defaultConfiguration;
// 交換掉NSURLSessionConfiguration的 protocolClasses方法
- (void)load;
// 還原初始化
- (void)unload;
@end
#import "FFSessionConfiguration.h"
#import <objc/runtime.h>
#import "CustomURLProtocol.h"
@implementation FFSessionConfiguration
+ (FFSessionConfiguration *)defaultConfiguration {
static FFSessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration=[[FFSessionConfiguration alloc] init];
});
return staticConfiguration;
}
- (instancetype)init {
self = [super init];
if (self) {
self.isExchanged = NO;
}
return self;
}
- (void)load {
self.isExchanged=YES;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)unload {
self.isExchanged=NO;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
if (!originalMethod || !stubMethod) {
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
method_exchangeImplementations(originalMethod, stubMethod);
}
- (NSArray *)protocolClasses {
// 如果還有其他的監控protocol,也可以在這里加進去
return @[[CustomURLProtocol class]];
}
@end
在使用時也很簡單,采用下面方式注冊
FFSessionConfiguration *sessionConfiguration = [FFSessionConfiguration defaultConfiguration];
[NSURLProtocol registerClass:[CustomURLProtocol class]];
if (![sessionConfiguration isExchanged]) {
[sessionConfiguration load];
}
3. 關于不能攔截WKWebView
??原因是WKWebView
在獨立于app
進程之外的進程中執行網絡請求,請求數據不經過主進程,因此,在WKWebView
上直接使用 NSURLProtocol
無法攔截請求。
具體可以參考NSURLProtocol對WKWebView的處理