NSURLProtocol的基本使用

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

你可能感兴趣的:(NSURLProtocol的基本使用)