NSURLProtocol 拦截网络请求

1,558 阅读5分钟

首先说下需求: 拦截iOS APP内的网络请求,如果HTTP请求成功, 接口会返回Code, msg, message 等字段,当code = 1001的时候,获取请求的host, path, msg等信息并记录下来。

拿到需求之后我们有方案

  1. 封装通用方法,当获取到请求数据之后,调用通用方法进行处理。

  2. 通过NSURLProtocol进行网络的请求拦截。

总结: 方案1 对当前的网络请求有侵入,对当前网络库的具有侵入性,不建议 方案2 通过NSURLProtocol对网络进行拦截,独立与当前网络请求,建议使用

先介绍下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 shareSession]初始化对象创建的网络请求,调用registerClass即可

//注册protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];

对于基于NSURLSession的网络请求,通过配置sessionWithConfiguration:delegate:delegateQueue:初始换对象的,需要通过runtime,将NSURLSessionConfiguration的属性protocolClasses的get方法hook掉,通过返回自定义的protocl,这样就能监控到网络请求。

  • (void)load { self.isExchanged=YES;
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; }
    - (NSArray *)protocolClasses {
    // 如果还有其他的监控protocol,也可以在这里加进去
    return @[[CustomURLProtocol class]];
    }

因此注册方法可以采用下面方法

[NSURLProtocol registerClass:[CustomURLProtocol class]];
[XXX load]

2. 子类必须要实现的方法

注册成功之后,在子类中去实现NSURLProtocol协议方法

//所有注册此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

NSURLSessionDataDelegate协议方法

//请求回调成功 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
//获取请求的网络数据 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

3. 详细说明

  • canInitWithRequest:

该方法会拿到request的对象,我们可以通过该方法的返回值来筛选request是否需要被NSURLProtocol做拦截处理。

  • (BOOL)canInitWithRequest:(NSURLRequest *)request{
    // 预加载模式,不拦截
    if([request containCustomFlag]){
    return NO;
    }
    //如果是已经拦截过的, 就放行(防止无限循环)
    if ([NSURLProtocol propertyForKey:KCustomInterceptTagForKey inRequest:request] ) {
    return NO;
    }
    return YES;
    }
  • canonicalRequestForRequest

//可选方法,对需要拦截的请求进行自定的处理 -(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{ //给拦截的请求添加标记位
NSMutableURLRequest *mutableRequest = [request mutableCopy];
return [mutableRequest copy];
}

  • requestIsCacheEquivalent:toRequest

主要是用来判断两个request是否相同

//主要是用来判断两个request是否相同,这个方法基本不常用 + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}

  • initWithRequest:cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id )client

    所有来源的请求都以NSURLRequest形式接收

//初始化protocol实例,所有来源的请求都以NSURLRequest形式接收
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client{
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}

  • startLoading

开始请求,在这里需要我们手动的把请求发出去,可以使用原生的NSURLSessionDataTask,也可以使用的第三方网络库,同时设置"NSURLSessionDataDelegate"协议,接收Server端的响应

  • (void)startLoading {
    NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:KCustomInterceptTagForKey inRequest: mutableRequest];
    // 在这里可以进行一系列的网络请求处理,比如添加请求头,处理参数等等
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    self.URLSession = session;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:mutableRequest];
    [task resume];
    }
  • stopLoading

请求被停止

  • (void)stopLoading {
    if(self.URLSession != nil){
    [self.URLSession invalidateAndCancel];
    self.URLSession = nil;
    }
    }
  • URLSession:task:didCompleteWithError:error

    请求完成

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if (error != nil) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}