美文网首页
使用 NSURLProtocol 拦截app内部的网络请求

使用 NSURLProtocol 拦截app内部的网络请求

作者: 朽木自雕也 | 来源:发表于2018-10-22 21:53 被阅读295次
    就在昨天被一个大佬问了一个问题,如何拦截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内的网络请求失败率,都可以用到。

    具体使用步骤

    1. 子类化 NSURLProtocol 为 CustomURLProtocol(子类名可以自己随便起)
    static NSString* const URLProtocolHandledKey = @"URLProtocolHandledKey";
    @interface CustomURLProtocol ()<NSURLConnectionDelegate>
    @property (nonatomic, strong) NSURLSession *session;
    @property (nonatomic, strong) NSURLRequest *myRequest;
    @end
    
    1. 在NSURLProtocol中,我们需要告诉它哪些网络请求是需要我们拦截的,这个是通过方法can​Init​With​Request:​来实现的,比如我们现在需要拦截全部的HTTP和HTTPS请求,那么这个逻辑我们就可以在can​Init​With​Request:​中来定义
    + (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];
    }
    
    1. 开始加载和结束加载
    //开始加载
    - (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];
    }
    
    1. 执行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];
    }
    

    在我们上层业务调用网络请求的时候,首先会调用我们的can​Init​With​Request:方法,询问是否对该请求进行处理,接着会调用我们的canonicalRequestForRequest:来自定义一个request,接着又会去调用can​Init​With​Request:询问自定义的request是否需要处理,我们又返回YES,然后又去调用了canonicalRequestForRequest:,这样,就形成了一个死循环了,这肯定是我们不希望看到的

    1. 在应用启动的时候注册进去
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [NSURLProtocol registerClass:CustomURLProtocol.class];
        return YES;
    }
    

    到这里基本差不多了,到这里NSURLProtocol的使用方法大家应该有所了解了。下面主要讲一下NSURLProtocol在
    使用过程中可能会遇到的坑,给自己以及需要的朋友留个提醒。

    1. 如果[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
    
    1. 上面一开始就已经说了,对于WebView的请求,目前NSURLProtocol还不能拦截WKWebView的请求,只能拦截UIWebview的,但后者好像AppStore已经不让审核通过了。

    2. NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。

    3. 使用NSURLProtocol时,在那两个类方法可以发送同步网络请求,而实例方法,如startLoading则进入死锁,直至超时,原因是执行实例方法所在的线程并没有启动runloop,而NSURLConnection这些网络请求需要依赖于runloop的,因此这些请求根本发不出去,所以必须使用异步请求,NSURLConnection/NSURLSession的异步请求的线程保证启动了runloop。

    以上就是我目前发现的坑,欢迎大家补充,也希望对大家开发有所帮助哈~。
    所幸的是NSURLProtocol对于大量并发的请求支持的还不错,不然就要弃用了~

    相关文章

      网友评论

          本文标题:使用 NSURLProtocol 拦截app内部的网络请求

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