美文网首页NSURLProtocol(页面缓存)
NSURLProtocol的基本使用

NSURLProtocol的基本使用

作者: yyggzc521 | 来源:发表于2019-11-21 19:37 被阅读0次

NSURLProtocol看起来像协议,其实是个抽象类,而且必须使用该类的子类,需要被注册,才能拦截网络请求。

不管你是通过UIWebView或第三方库 AFNetworking,他们都是基于 NSURLSession实现的,因此可以通过NSURLProtocol做自定义的操作

\color{red}{应用场景:}

  • 广告过滤或重定向
  • APP内所有请求增加公共头
  • 某个API进行访问统计
  • 统计APP内的网络请求失败率
  • 忽略网络请求,使用本地缓存
  • 自定义网络请求的返回结果
  • 拦截图片加载请求,转为从本地文件加载
  • 快速进行测试环境的切换
  • 网络的缓存处理(H5离线包 和 网络图片缓存)

目前WKWebView无法被NSURLProtocol拦截

\color{red}{1、首先需创建一个NSURLProtocol子类,在使用时进行注册:}

   [NSURLProtocol registerClass:[QURLProtocol class]];

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

\color{red}{2、子类中重写必须实现的5个方法:}

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

\color{blue}{canInitWithRequest方法中设置要拦截的请求}

+ (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;
}

\color{blue}{startLoading对拦截的地址进行重定向}

- (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];
        
    }
}

\color{blue}{stopLoading方法中对任务、请求进行取消:}

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

\color{blue}{还有两个方法,没特殊需求重写父类即可:}

//返回规范的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<Class> *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

\color{red}{参考资料:}
使用NSURLProtocol拦截APP内的网络请求
NSURLProtocol详解和应用
NSURLProtocol对WKWebView的处理
NSURLProtocol之网络拦截
防劫持 重定向到ip地址
让WKWebView 支持 NSURLProtocol
WKWebView加载不受信任的https (因用到IP地址加端口号去请求数据)
拦截图片加载请求,转为从本地文件加载
NSURLProtocol 的使用和封装
iOS应用内抓包、NSURLProtocol 拦截 APP 内的网络请求
Demo1
Demo2

相关文章

网友评论

    本文标题:NSURLProtocol的基本使用

    本文链接:https://www.haomeiwen.com/subject/xuttlqtx.html