NSURLProtocol的基本使用

NSURLProtocol看起來像協(xié)議,其實(shí)是個(gè)抽象類,而且必須使用該類的子類,需要被注冊,才能攔截網(wǎng)絡(luò)請求。

不管你是通過UIWebView或第三方庫 AFNetworking,他們都是基于 NSURLSession實(shí)現(xiàn)的,因此可以通過NSURLProtocol做自定義的操作

\color{red}{應(yīng)用場景:}

  • 廣告過濾或重定向
  • APP內(nèi)所有請求增加公共頭
  • 某個(gè)API進(jìn)行訪問統(tǒng)計(jì)
  • 統(tǒng)計(jì)APP內(nèi)的網(wǎng)絡(luò)請求失敗率
  • 忽略網(wǎng)絡(luò)請求,使用本地緩存
  • 自定義網(wǎng)絡(luò)請求的返回結(jié)果
  • 攔截圖片加載請求,轉(zhuǎn)為從本地文件加載
  • 快速進(jìn)行測試環(huán)境的切換
  • 網(wǎng)絡(luò)的緩存處理(H5離線包 和 網(wǎng)絡(luò)圖片緩存)

目前WKWebView無法被NSURLProtocol攔截

\color{red}{1、首先需創(chuàng)建一個(gè)NSURLProtocol子類,在使用時(shí)進(jìn)行注冊:}

   [NSURLProtocol registerClass:[QURLProtocol class]];

- (void)dealloc {//記得釋放
    [NSURLProtocol unregisterClass:[KCURLProtocol class]];
}

\color{red}{2、子類中重寫必須實(shí)現(xiàn)的5個(gè)方法:}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;

\color{blue}{canInitWithRequest方法中設(shè)置要攔截的請求}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    //已經(jīng)攔截過的就不再攔截,避免死循環(huán)
    if ([NSURLProtocol propertyForKey:QZProtocolKey inRequest:request]) {
        return NO;
    }

     //攔截所有的http和HTTPS請求
    if ([request.URL.scheme isEqualToString:@"http"] || [request.URL.scheme isEqualToString:@"https"]) { 
        return YES;
    }
 
   //攔截百度,這里可以使用isEqualToString進(jìn)行精準(zhǔn)攔截
    if ([[request.URL absoluteString] containsString:@"www.baidu.com"]) {
        return YES;
    }
    return NO;
}

\color{blue}{startLoading對攔截的地址進(jìn)行重定向}

- (void)startLoading {
    
    //標(biāo)記,下次不攔截自己設(shè)置的
    [NSURLProtocol setProperty:@(YES) forKey:QZProtocolKey inRequest:[self.request mutableCopy]];
 
    //重定向
    if ([[self.request.URL absoluteString] isEqualToString:@"https://www.baidu.com/"]) {
      
        NSString*url = @"http://www.itdecent.cn/";
        NSURLRequest*myRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
        
        NSURLSessionConfiguration *configuration =
        [NSURLSessionConfiguration defaultSessionConfiguration];
        
        self.queue = [[NSOperationQueue alloc] init];
        self.queue.maxConcurrentOperationCount = 1;
        self.queue.name = @"com.Qinz.cn";
        
        NSURLSession *session =
        [NSURLSession sessionWithConfiguration:configuration
                                      delegate:self
                                 delegateQueue:self.queue];
        //偷梁換柱
        self.task = [session dataTaskWithRequest:myRequest];
        [self.task resume];
        
    }
}

\color{blue}{stopLoading方法中對任務(wù)、請求進(jìn)行取消:}

- (void)stopLoading{
     [self.task cancel];
     [self.connection cancel];
     self.connection = nil;
}

\color{blue}{還有兩個(gè)方法,沒特殊需求重寫父類即可:}

//返回規(guī)范的request  自定義當(dāng)前請求request,如果不需要自定義,直接返回就行
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{   
    return request;
}
/**
這個(gè)方法主要用來判斷兩個(gè)請求是否是同一個(gè)請求,如果是,則可以使用緩存數(shù)據(jù),通常只需要調(diào)用父類的實(shí)現(xiàn)即可,默認(rèn)為YES
 */
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
}

對于每個(gè)NSURLProtocol的子類,都有一個(gè)client,通過它來對iOS的網(wǎng)絡(luò)加載系統(tǒng)進(jìn)行一系列的操作,比如,通知收到response或者錯(cuò)誤的網(wǎng)絡(luò)請求等等

NSURLSession的網(wǎng)絡(luò)請求,通過shared得到的session的網(wǎng)絡(luò)請求都能監(jiān)聽到,但通過方法sessionWithConfiguration:delegate:delegateQueue:得到的session,是不能監(jiān)聽到的,原因在NSURLSessionConfiguration,NSURLSessionConfiguration有個(gè)屬性

@property (nullable, copy) NSArray<Class> *protocolClasses;

這是個(gè)NSURLProtocol數(shù)組,監(jiān)控網(wǎng)絡(luò)是通過注冊NSURLProtocol進(jìn)行的,通過sessionWithConfiguration:delegate:delegateQueue:得到的session,它的configuration中已經(jīng)有一個(gè)NSURLProtocol,所以不會(huì)走我們的protocol,怎么解決這個(gè)問題呢?

很簡單,將NSURLSessionConfiguration屬性protocolClasses的get方法hook掉,返回我們自己的protocol

- (void)load {
    
    self.isSwizzle=YES;
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}

- (void)unload {
    
    self.isSwizzle=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 {
    
    return @[[PPSURLProtocol class]];
    //如果還有其他的監(jiān)控protocol,也可以在這里加進(jìn)去
}

啟動(dòng)的時(shí),將這個(gè)方法替換掉,移除監(jiān)聽時(shí),恢復(fù)之前的方法

至此,監(jiān)聽就完成了,如果需要將這所有的監(jiān)聽存起來,在protocol的start或者stop中獲取到request和response,將它們存儲(chǔ)起來。

需要說明的是,據(jù)蘋果官方說明,因?yàn)檎埱髤?shù)可能會(huì)很大,為了保證性能,請求參數(shù)是沒有被攔截掉的,就是post的HTTPBody是沒有的

為了解決這個(gè)問題,可以把Body數(shù)據(jù)放到Header中,不過Header的大小好像有限制的,2M是沒有問題,不過超過10M就直接Request timeout了。。。當(dāng)Body數(shù)據(jù)為二進(jìn)制數(shù)據(jù)時(shí)這招也沒轍了,因?yàn)镠eader都是文本數(shù)據(jù),另一種方案就是用一個(gè)NSDictionary或NSCache保存沒有請求的Body數(shù)據(jù),用URL為key

\color{red}{參考資料:}
使用NSURLProtocol攔截APP內(nèi)的網(wǎng)絡(luò)請求
NSURLProtocol詳解和應(yīng)用
NSURLProtocol對WKWebView的處理
NSURLProtocol之網(wǎng)絡(luò)攔截
防劫持 重定向到ip地址
讓W(xué)KWebView 支持 NSURLProtocol
WKWebView加載不受信任的https (因用到IP地址加端口號去請求數(shù)據(jù))
攔截圖片加載請求,轉(zhuǎn)為從本地文件加載
NSURLProtocol 的使用和封裝
iOS應(yīng)用內(nèi)抓包、NSURLProtocol 攔截 APP 內(nèi)的網(wǎng)絡(luò)請求
Demo1
Demo2

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

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

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