NSURLProtocol看起来像协议,其实是个抽象类,而且必须使用该类的子类,需要被注册,才能拦截网络请求。
不管你是通过UIWebView或第三方库 AFNetworking,他们都是基于 NSURLSession实现的,因此可以通过NSURLProtocol做自定义的操作
- 广告过滤或重定向
- APP内所有请求增加公共头
- 某个API进行访问统计
- 统计APP内的网络请求失败率
- 忽略网络请求,使用本地缓存
- 自定义网络请求的返回结果
- 拦截图片加载请求,转为从本地文件加载
- 快速进行测试环境的切换
- 网络的缓存处理(H5离线包 和 网络图片缓存)
目前WKWebView无法被NSURLProtocol拦截
[NSURLProtocol registerClass:[QURLProtocol class]];
- (void)dealloc {//记得释放
[NSURLProtocol unregisterClass:[KCURLProtocol class]];
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//已经拦截过的就不再拦截,避免死循环
if ([NSURLProtocol propertyForKey:QZProtocolKey inRequest:request]) {
return NO;
}
//拦截所有的http和HTTPS请求
if ([request.URL.scheme isEqualToString:@"http"] || [request.URL.scheme isEqualToString:@"https"]) {
return YES;
}
//拦截百度,这里可以使用isEqualToString进行精准拦截
if ([[request.URL absoluteString] containsString:@"www.baidu.com"]) {
return YES;
}
return NO;
}
- (void)startLoading {
//标记,下次不拦截自己设置的
[NSURLProtocol setProperty:@(YES) forKey:QZProtocolKey inRequest:[self.request mutableCopy]];
//重定向
if ([[self.request.URL absoluteString] isEqualToString:@"https://www.baidu.com/"]) {
NSString*url = @"https://www.jianshu.com/";
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];
}
}
- (void)stopLoading{
[self.task cancel];
[self.connection cancel];
self.connection = nil;
}
//返回规范的request 自定义当前请求request,如果不需要自定义,直接返回就行
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
return request;
}
/**
这个方法主要用来判断两个请求是否是同一个请求,如果是,则可以使用缓存数据,通常只需要调用父类的实现即可,默认为YES
*/
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
}
对于每个NSURLProtocol的子类,都有一个client,通过它来对iOS的网络加载系统进行一系列的操作,比如,通知收到response或者错误的网络请求等等
NSURLSession的网络请求,通过shared得到的session的网络请求都能监听到,但通过方法sessionWithConfiguration:delegate:delegateQueue:得到的session,是不能监听到的,原因在NSURLSessionConfiguration,NSURLSessionConfiguration有个属性
@property (nullable, copy) NSArray *protocolClasses;
这是个NSURLProtocol数组,监控网络是通过注册NSURLProtocol进行的,通过sessionWithConfiguration:delegate:delegateQueue:得到的session,它的configuration中已经有一个NSURLProtocol,所以不会走我们的protocol,怎么解决这个问题呢?
很简单,将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]];
//如果还有其他的监控protocol,也可以在这里加进去
}
启动的时,将这个方法替换掉,移除监听时,恢复之前的方法
至此,监听就完成了,如果需要将这所有的监听存起来,在protocol的start或者stop中获取到request和response,将它们存储起来。
需要说明的是,据苹果官方说明,因为请求参数可能会很大,为了保证性能,请求参数是没有被拦截掉的,就是post的HTTPBody是没有的
为了解决这个问题,可以把Body数据放到Header中,不过Header的大小好像有限制的,2M是没有问题,不过超过10M就直接Request timeout了。。。当Body数据为二进制数据时这招也没辙了,因为Header都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key
使用NSURLProtocol拦截APP内的网络请求
NSURLProtocol详解和应用
NSURLProtocol对WKWebView的处理
NSURLProtocol之网络拦截
防劫持 重定向到ip地址
让WKWebView 支持 NSURLProtocol
WKWebView加载不受信任的https (因用到IP地址加端口号去请求数据)
拦截图片加载请求,转为从本地文件加载
NSURLProtocol 的使用和封装
iOS应用内抓包、NSURLProtocol 拦截 APP 内的网络请求
Demo1
Demo2