NSURLProtocol hook網(wǎng)絡(luò)

1. NSURLProtocol類

NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統(tǒng) (URL Loading System)的行為,URL Loading System里有許多類用于處理URL請(qǐng)求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等


image.png

可以攔截的網(wǎng)絡(luò)請(qǐng)求包括NSURLSession,NSURLConnection以及UIWebVIew,基于CFNetwork的網(wǎng)絡(luò)請(qǐng)求,以及WKWebView的請(qǐng)求是無法攔截的,WKWebView基于Webkit。

NSURLProtocol是一個(gè)抽象類,不能直接創(chuàng)建使用,需要繼承使用,使用的時(shí)候需要調(diào)用registerClass:, [NSURLProtocol registerClass:[KCURLProtocol class]]

image.png

image.png

NSURLProtocol用處:

  • 重定向網(wǎng)絡(luò)請(qǐng)求(可以解決電信的DNS域名劫持問題)
  • 忽略網(wǎng)絡(luò)請(qǐng)求,使用本地緩存
  • 自定義網(wǎng)絡(luò)請(qǐng)求的返回結(jié)果Response
  • 攔截圖片加載請(qǐng)求,轉(zhuǎn)為從本地文件加載
  • 一些全局的網(wǎng)絡(luò)請(qǐng)求設(shè)置
  • 快速進(jìn)行測(cè)試環(huán)境的切換
  • 過濾掉一些非法請(qǐng)求
  • 網(wǎng)絡(luò)的緩存處理(H5離線包 和 網(wǎng)絡(luò)圖片緩存)
  • 可以攔截UIWebView,基于系統(tǒng)的NSURLConnection或者NSURLSession進(jìn)行封裝的網(wǎng)絡(luò)請(qǐng)求。目前WKWebView無法被NSURLProtocol攔截。
  • 當(dāng)有多個(gè)自定義NSURLProtocol注冊(cè)到系統(tǒng)中的話,會(huì)按照他們注冊(cè)的反向順序依次調(diào)用URL加載流程。當(dāng)其中有一個(gè)NSURLProtocol攔截到請(qǐng)求的話,后續(xù)的NSURLProtocol就無法攔截到該請(qǐng)求。

2. NSURLProtocol的使用主要是5個(gè)步驟:

  1. 注冊(cè)
    [NSURLProtocol registerClass:[KCURLProtocol class]];
  1. 攔截
    NSURLConnection UIWebview請(qǐng)求攔截
    + (BOOL)canInitWithRequest:(NSURLRequest *)request;
    NSURLSession的task攔截
    + (BOOL)canInitWithTask:(NSURLSessionTask *)task;
    這兩個(gè)方法是注冊(cè)后,NSURLProtocol就會(huì)通過這個(gè)方法確定參數(shù)request、task是否需要被處理,YES需要被處理,NO不需要被處理,可以做重定向、監(jiān)聽
  1. 轉(zhuǎn)發(fā)
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
    通過該方法你可以簡(jiǎn)單的直接返回request,但可以在這里修改request,比如修改header,修改host等,并返回一個(gè)新的request,這是一個(gè)抽象方法,子類必須實(shí)現(xiàn)
    - (void)startLoading;
    把處理過的request重新發(fā)送出去

4.回調(diào)
這四個(gè)方法來回調(diào)給原來發(fā)送網(wǎng)絡(luò)請(qǐng)求的地方。
[self.client URLProtocol:self didFailWithError:error];
[self.client URLProtocolDidFinishLoading:self];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];

  1. 結(jié)束
    - (void)stopLoading;
    [NSURLProtocol unregisterClass:[KCURLProtocol class]];

根據(jù)request從系統(tǒng)的緩存中構(gòu)建protocol,沒有緩存可能為nil,
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

處理過的請(qǐng)求可以通過為請(qǐng)求設(shè)置關(guān)聯(lián),下次不再處理,防止無限循環(huán)
+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request

判斷2個(gè)請(qǐng)求是否相等,相等的話可以使用緩存數(shù)據(jù)
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b

3. 攔截重定向演示

3.1 UIWebView 攔截

KCURLProtocol中:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    // 攔截百度的logo
    if ([[request.URL absoluteString] isEqualToString:@"https://m.baidu.com/static/index/plus/plus_logo_web.png"]) {
        return YES;
    }
    return NO;
}
- (void)startLoading
{
    // 攔截改成自己數(shù)據(jù)
    if ([[self.request.URL absoluteString] isEqualToString:@"https://m.baidu.com/static/index/plus/plus_logo_web.png"]) {
        NSString *fileName = [[NSBundle mainBundle] pathForResource:@"lufei.jpg" ofType:@""];
        NSData *data = [NSData dataWithContentsOfFile:fileName];
        [self.client URLProtocol:self didLoadData:data];//回調(diào)回去
    }
}
image.png
3.2 NSURLSession 攔截
image.png

NSURLSession需要給NSURLSessionConfiguration對(duì)象的protocolClasses屬性添加自己的協(xié)議,把系統(tǒng)的也加回去

- (void)startSessionHook
 {
    NSString *url  = @"http://www.baidu.com";
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:6];
    [arr addObject:[NSClassFromString(@"KCURLProtocol") class]];
    [arr addObjectsFromArray:config.protocolClasses];
    config.protocolClasses = arr;
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
  
    [dataTask resume];
}
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task
{
    NSLog(@"task---%@",task);
    if ([task.originalRequest.URL.absoluteString isEqualToString:@"http://www.baidu.com"]) {
        return YES;
    }
    return NO;
}
//轉(zhuǎn)發(fā)
- (void)startLoading{
    
    NSMutableURLRequest *request = [self.request mutableCopy];
    if ([request.URL.absoluteString isEqualToString:@"http://www.baidu.com"]) {
        request.URL = [NSURL URLWithString:@"http://www.itdecent.cn/"];
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
        
        [dataTask resume];
    }
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    completionHandler(NSURLSessionResponseAllow);
}
//回調(diào)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    // 打印返回?cái)?shù)據(jù)
    NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (dataStr) {
        NSLog(@"***截取數(shù)據(jù)***: %@", 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];
    }
}
3.3 AFN 攔截

AFN也是基于NSURLSession,我們可以新建一個(gè)NSURLSession的子類,在load方法里面hook protocolClasses的getter方法,這樣所有的基于session的都能攔截測(cè)試實(shí)際上是hook __NSCFURLSessionConfiguration的getter方法,hook NSURLSessionConfiguration的getter不管用

+ (void)load
{
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
    method_setImplementation(originalMethod, class_getMethodImplementation(self, @selector(my_protocolClasses)));
}
- (NSArray<Class> *)my_protocolClasses
{
    NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:[NSClassFromString(@"KCURLProtocol") class], [NSClassFromString(@"_NSURLHTTPProtocol") class],[NSClassFromString(@"_NSURLDataProtocol") class],[NSClassFromString(@"_NSURLFTPProtocol") class],[NSClassFromString(@"_NSURLFileProtocol") class],[NSClassFromString(@"NSAboutURLProtocol") class],nil];
    return arr;
}

4. 多個(gè)NSURLProtocol

如果注冊(cè)了兩個(gè)NSURLProtocol,protocols的遍歷是反向的,也就是最后注冊(cè)的protocol會(huì)被優(yōu)先判斷。


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容