就在昨天被一个大佬问了一个问题,如何拦截app内部的所有网络请求,并且在请求的头部动态添加一些内容,之前有看过这方面的资料,但是自己没有去实现过,产品也没这个需求,现在被人问到了,也就来仔细研究一下。APP内的网络请求的监控,相信很多APP内都有这个模块,通过监控APP内的网络请求,观察各个API的稳定性。这些数据,一般我们都会先收集起来,在一段时间内,上传到服务器。在iOS中,出了WK的网络请求,其他的所有网络请求都可以通过NSURLProtocol来拦截监控。
在iOS中苹果提供了NSURLConnection、NSURLSession等优秀的网路接口供我们来调用,开源社区也有很多的开源库,如之前的ASIHttpRequest 现在的AFNetworking和Alamofire,我们接下来介绍的NSURLProtocol,都可以监控到这些开源库的网络请求
NSURLProtocol是iOS网络加载系统中很强的一部分,它其实是一个抽象类,我们可以通过继承子类化来拦截APP中的网络请求。
NSURLProtocol 是真的很强,可以拦截应用内几乎所有的网络请求(除了WKWebView),并可以修改请求头,返回client任意自定义的数据等等,据说很多做网络缓存都是利用这个类的。
我们的APP内的所有请求都需要增加公共的头,像这种我们就可以直接通过NSURLProtocol来实现,当然实现的方式有很多种 ;再比如我们需要将APP某个API进行一些访问的统计,我们需要统计APP内的网络请求失败率,都可以用到。
具体使用步骤
- 子类化 NSURLProtocol 为 CustomURLProtocol(子类名可以自己随便起)
static NSString* const URLProtocolHandledKey = @"URLProtocolHandledKey";
@interface CustomURLProtocol ()<NSURLConnectionDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLRequest *myRequest;
@end
- 在NSURLProtocol中,我们需要告诉它哪些网络请求是需要我们拦截的,这个是通过方法canInitWithRequest:来实现的,比如我们现在需要拦截全部的HTTP和HTTPS请求,那么这个逻辑我们就可以在canInitWithRequest:中来定义
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//避免死循环
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
if ([request.URL.scheme isEqualToString:@"http"]
|| [request.URL.scheme isEqualToString:@"https"]) {
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
//标记一下,避免死循环
[NSURLProtocol setProperty:@YES
forKey:URLProtocolHandledKey
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
- 开始加载和结束加载
//开始加载
- (void)startLoading {
NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
self.myRequest = request;
}
//结束加载
- (void)stopLoading {
[self.connection cancel];
}
- 执行connection代理方法,实现数据监听
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[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];
}
在我们上层业务调用网络请求的时候,首先会调用我们的canInitWithRequest:方法,询问是否对该请求进行处理,接着会调用我们的canonicalRequestForRequest:来自定义一个request,接着又会去调用canInitWithRequest:询问自定义的request是否需要处理,我们又返回YES,然后又去调用了canonicalRequestForRequest:,这样,就形成了一个死循环了,这肯定是我们不希望看到的
- 在应用启动的时候注册进去
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:CustomURLProtocol.class];
return YES;
}
到这里基本差不多了,到这里NSURLProtocol的使用方法大家应该有所了解了。下面主要讲一下NSURLProtocol在
使用过程中可能会遇到的坑,给自己以及需要的朋友留个提醒。
- 如果[NSURLSession sharedSession]创建的session对象是可以拦截的,如果是NSURLSession是使用这两个方法创建的就拦截不到了
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration
delegate:(nullable id <NSURLSessionDelegate>)delegate
delegateQueue:(nullable NSOperationQueue *)queue;
也不是说一定拦击不到,点进 NSURLSessionConfiguration 类的文件中可以看到如下说明
/* An optional array of Class objects which subclass NSURLProtocol.
The Class will be sent +canInitWithRequest: when determining if
an instance of the class can be used for a given URL scheme.
You should not use +[NSURLProtocol registerClass:], as that
method will register your class with the default session rather
than with an instance of NSURLSession.
Custom NSURLProtocol subclasses are not available to background
sessions.
*/
@property (nullable, copy) NSArray<Class> *protocolClasses;
一个可选的类对象数组,它是NSURLProtocol的子类。类将被发送+canInitWithRequest:当确定是否该类的实例可以用于给定的URL模式。因此,您不应该使用+[NSURLProtocol registerClass:]方法将您的类注册为默认会话而不是NSURLSession的实例。自定义NSURLProtocol子类对后台不可用会话。
我有一个大胆想法,就是写一个 NSURLSessionConfiguration 的分类,然后也写个protocolClasses属性的get方法,由于在方法调用的时候会优先调用分类方法,所以NSURLSessionConfiguration类在调用protocolClasses时就会调用到分类中的方法,代码如下
@implementation NSURLSessionConfiguration (Custom)
- (NSArray<Class> *)protocolClasses {
return @[CustomURLProtocol.class];
}
@end
上面一开始就已经说了,对于WebView的请求,目前NSURLProtocol还不能拦截WKWebView的请求,只能拦截UIWebview的,但后者好像AppStore已经不让审核通过了。
NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。
使用NSURLProtocol时,在那两个类方法可以发送同步网络请求,而实例方法,如startLoading则进入死锁,直至超时,原因是执行实例方法所在的线程并没有启动runloop,而NSURLConnection这些网络请求需要依赖于runloop的,因此这些请求根本发不出去,所以必须使用异步请求,NSURLConnection/NSURLSession的异步请求的线程保证启动了runloop。
以上就是我目前发现的坑,欢迎大家补充,也希望对大家开发有所帮助哈~。
所幸的是NSURLProtocol对于大量并发的请求支持的还不错,不然就要弃用了~