DoKit中网络拦截的使用总结

3,476 阅读9分钟

前言

之前在滴滴的Doraemon团队进行网络相关的技术开发,其中使用到NSURLProtocol相关的领域,写下相关认识。关于开源项目DoraemonKit,是一款功能齐全的客户端( iOS 、Android、微信小程序 )研发助手,能让你的开发效率得到明显的提高,详见《滴滴DoKit2.0 - 泛前端开发者的百宝箱》

NSURLProtocol能够让你去重新定义苹果的URL加载系统 (URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个抽象类,我们要使用它的时候需要创建它的一个子类,并且需要被注册。

从上图中我们可以看出在URL加载系统 (URL Loading System)中,NSURLProtocol作为Client和Server的中间层,接收Client发送的Request,将其发送至Server端,并接收Server端发送的Response,将数据传回Client端;而且NSURLProtocol是一个抽象类,提供了处理 URL 加载的基础设施。通过实现自定义的 NSURLProtocol 子类,可以让我们的 app 支持自定义的数据传输协议。DoKit借助它,在不改动应用在网络调用上的其他部分,就可以改变 URL 加载行为的细节,从而做到mock数据、弱网限速、流量数据展示等功能的实现。

一、DoraemonNSURLProtocol

和大多数使用NSURLProtocol的步骤相同,DoKit在使用NSURLProtocol也是经过 注册—>拦截—>转发—>回调—>结束 5大步骤,这五大步骤中,DoKit的处理和常见的处理率有差异,下面是我对DoKit在网络拦截中的一些总结,重点介绍数据mock功能和弱网功能。

1.1 注册:

DoKit在继承NSURLProtocol之后,借助DoraemonNetworkInterceptor的代理,实现方法:

- (BOOL)shouldIntercept; 

然后在想要进行网络拦截的地方加入:

[[DoraemonNetworkInterceptor shareInstance] addDelegate: self];

图解:
每一个想进行网络拦截的功能都得先实现DoraemonNetworkInterceptorDelegate的代理方法,然后在DoraemonNetworkInterceptor每次addDelegate后会判断代理是否需要shouldIntercept,返回YES才注册DoraemonNSURLProtocol。 DoKit在DoraemonNetworkInterceptor里面遍历代理和NSURLProtocol对遍历注册拦截子类的处理思想一样,只不过遍历方向不同;如果代理里有一个存在需要拦截网络,就将DoKit里面唯一的NSURLProtocol子类DoraemonNSURLProtocol注册到NSURLProtocol里。这就避免了注册多个NSURLProtocol子类会逆序去执行,也就是先注册的子类后执行的问题。 在结束拦截的地方加入:

[[DoraemonNetworkInterceptor shareInstance] removeDelegate: self];

此处DoKit移除相应的代理,并把DoraemonNSURLProtocol注销掉。

1.2 拦截:

  • 拦截网络第一步进入:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request

该方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象。DoKit只对存在需要拦截的情况下,而且是http和https的非文件类型的网络请求进行拦截。

  • 拦截网络第二步进入:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

该方法可以对请求数据进行自定义封装。DoKit在该方法中进行数据mock相关操作,下文会详细介绍。

1.3 转发:

转发网络请求主要在startLoading中进行的。DoKit通过拦截到的request来获取NSURLSessionDataTask开始加载请求,NSURLSessionDataTask继承于NSURLSessionTask,而NSURLSession可以管理多个NSURLSessionTask。而且DoKit的task是通过封装request到NSURLSessionConfiguration得到的NSURLSessionDataTask,然后进行网络加载。

[self.task resume];

创建好的NSURLSessionDataTask是suspend状态的,调用resume才能开始执行。

1.4 回调:

DoKit的回调做了比较缜密的工作,先是在DoraemonNSURLProtocol里实现NSURLSessionDataTaskDelegate的相关方法,然后在DoraemonURLSessionDemuxTaskInfo声明NSURLSessionDataTaskDelegate对象并实现相应的回调函数,然后在转发新建NSURLSessionDataTask的时候将DoraemonNSURLProtocol赋予该代理对象。如图 图解:
DoraemonNSURLProtocol继承于NSURLProtocol和NSURLSessionDataDelegate,然后作为DoraemonURLSessionDemuxTaskInfo的一个属性,所以DoraemonNSURLProtocol的NSURLSession的回调先到DoraemonURLSessionDemuxTaskInfo,然后在唤起DoraemonNSURLProtocol的相关函数。 DoraemonNetworkInterceptor主要是给DoraemonNSURLProtocol做注册和 注销的工作,每次先判断shouldIntercept,再进行具体的拦截转发工作。

这样NSURLSessionDataTask就会在DoraemonURLSessionDemuxTaskInfo实现的回调函数进行回调,DoraemonURLSessionDemuxTaskInfo先检查task是否存在,保证每个拦截之后的task都是正确的,然后再到DoraemonURLProtocol里面处理:(didReceiveData例子)

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    DoraemonURLSessionDemuxTaskInfo *taskInfo;
    taskInfo = [self taskInfoForTask:dataTask];
    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [taskInfo performBlock:^{            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
        }];
    }
}

既是面向切面的编程,就不能影响到原来网络请求的逻辑。所以上一步将网络请求转发出去以后,当收到网络请求的返回,还需要再将返回值返回给原来发送网络请求的地方。 在处理方法里要将相应的数据放回到client:(didReceiveData例子)

[self.client URLProtocol:self didLoadData:data];

1.5 结束:

DoKit在startLoading的转发请求是用NSURLSessionDataTask,所以在startLoading的时候使用task自带的cancle:

    if (self.task != nil) {
        [self.task cancel];
        self.task = nil;
    }

二、数据mock功能:

数据mock功能是DoKit的一大亮点,DoKit致力于提高开发效率,而在进行前后端交互的时候,终端总要进行一些边缘数据的测试,就会与后端开发人员发生交流,甚至delay终端终端的开发进度。如果接入DoKit就能很好的规避这样的风险存在,DoKit的数据mock功能提供一套基于App网络拦截的接口Mock方案,无需修改代码即可完成对于接口数据的Mock。

  1. 支持原生接口数据作为Mock模板 拦截App端原生接口的数据请求返回结果,支持上传该数据到我们的平台端,让我们可以更高效地获取Mock数据模版。
  2. 支持多场景结果切换 对于同一个接口数据Mock,支持不同的场景的结果返回,各个场景灵活切换控制。
  3. 实用场景丰富 开发前无需等待后端同学开发完成,即可使用接口进行开发;开发中可以构造各种场景的数据,提高提测质量;开发后测试同学可以构造各种异常数据,复现问题更简便,更高效的回归各种场景,更高效的提高研发流程。

2.1 使用:

  1. 去DoKit平台获取一个产品ID,然后将DoKit的接入方式
[[DoraemonManager shareInstance] install];

换成带有具体productID:(例)749a060b5e48dd77cfee680be7b1b7

[[DoraemonManager shareInstance] installWithPid:@"productID"];
  1. 点击终端DoKit面板中的数据Mock功能,看到平台产品下的场景,具体操作请查看DoKit平台的使用中心
  2. 在终端切换场景,然后加载网络请求,就会发现终端的响应数据是对应场景下的自定义数据,避免了和后端人员的口舌之交,最重要的是没有delay进度。

2.2 实现:

在前面的基础上,DoKit的数据mock功能主要是在canonicalRequestForRequest :

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:kDoraemonProtocolKey inRequest:mutableReqeust];
    if ([[DoraemonMockManager sharedInstance] needMock:request]) {
        NSString *sceneId = [[DoraemonMockManager sharedInstance] getSceneId:request];
        NSString *urlString = [NSString stringWithFormat:@"https://mock.dokit.cn/api/app/scene/%@",sceneId];
        DoKitLog(@"MOCK URL == %@",urlString);
        mutableReqeust = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
        dispatch_async(dispatch_get_main_queue(), ^{
            [DoraemonToastUtil showToastBlack:[NSString stringWithFormat:@"mock url = %@",request.URL.absoluteURL] inView:[UIViewController rootViewControllerForKeyWindow].view];
        });

    }
    return [mutableReqeust copy];
}

该方法里面将拦截到的网络请求与当前被mock的接口进行匹配,匹配条件为path+query,然后再带上相应的场景ID,重定向到www.dokit.cn 平台上请求数据,就可以返回对应场景的数据。轻轻松松提高开发效率。

三、流量感知:

这是对流量的实时感知功能,当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,对App内部的网络加载进行拦截转发。当NSURLProtocol的调用stopLoading时,流量感知功能部分回获取该 NSURLProtocol实例的request和response,然后处理并以折线图显示。

3.1 实现:

- (void)stopLoading{
    assert(self.clientThread != nil);
    assert([NSThread currentThread] == self.clientThread);
    [[DoraemonNetworkInterceptor shareInstance] handleResultWithData:self.data response: self.response request:self.request error:self.error startTime:self.startTime];
    if (self.task != nil) {
        [self.task cancel];
        self.task = nil;
    }
}

以上就是对流量感知的数据获取,在DoraemonNetworkInterceptor的handleResultWithData方法里调用对应流量感知delegate的处理。 图解:
DoraemonNetFlowManager调用canInterceptNetFlow主要是对DoraemonNSURLProtocol的注册和注销。当有网络请求的时候,DoraemonNSURLProtocol会先拦截到该网络请求,然后到DoraemonNetworkInterceptor判断是否需要处理。通过DoraemonNSURLProtocol的一系列处理之后,在stopLoading里面回调代理的方法,DoraemonNetFlowManager拿到数据,然后实时显示出来。

四、大图检测:

大图检测部分和流量感知的数据处理一样,拦截到URL Loading System的数据后,在实现DoraemonNetworkInterceptorDelegate的方法doraemonNetworkInterceptorDidReceiveData里判断是否存在大图:

if (![response.MIMEType hasPrefix:@"image/"]) {
        return;
    }
if ([DoraemonUrlUtil getResponseLength:(NSHTTPURLResponse *)response data:data] < self.minimumDetectionSize) {
        return;
    }

大图检测的大小在DoKit接入的时候就可以设置,如果没有设置大小,DoKit也会初始化大小为:500 * 1024,图片超过限制大小,就会记录下来,然后在检测记录中可以查看到图片、大小还有加载地址。

五、弱网功能:

弱网功能主要是能模拟出网络差的情况下达到的效果。在当今社会,网络流畅,弱网的复现变得困难,有些团队不考虑到弱网case的处理。但是大公司肯定要确保弱网情况下也要不阻碍主流程,DoKit也是考虑到存在弱网的需求,开发出模拟弱网的功能。弱网功能主要有断网、超时、限速三大功能。

5.1 断网和超时:

断网和超时的处理,是模拟出来的,当response数据回调到DoraemonURLSessionDemuxTaskInfo的didReceiveResponse,然后唤起DoraemonNSURLProtocol的didReceiveResponse方法,断网、超时处理就是:

if ([DoraemonNetworkInterceptor shareInstance].weakDelegate){
        if(DoraemonWeakNetwork_OutTime == [[DoraemonNetworkInterceptor shareInstance].weakDelegate weakNetSelecte]){
        DoKitLog(@"yd Outtime Net");
        [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:NSCocoaErrorDomain code:NSURLErrorTimedOut userInfo:nil]];
            result = NO;
    }else if(DoraemonWeakNetwork_Break == [[DoraemonNetworkInterceptor shareInstance].weakDelegate weakNetSelecte]){
            DoKitLog(@"yd Break Net");
        [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:NSCocoaErrorDomain code:NSURLErrorNotConnectedToInternet userInfo:nil]];
            result = NO;
        }
    }

其中直接返回断网和超时的错误码,接下来就到DoraemonNSURLProtocol的stopLoading方法,请求结束,断网和超时的模拟完成。

5.2 限速:

流量分为上行流量和下行流量,所以限速也分为上行流量限速和下行流量限速。上行流量的限速是在拦截之后,转发之前,获取request的httpBody:

if(DoraemonWeakNetwork_WeakSpeed == [[DoraemonNetworkInterceptor shareInstance].weakDelegate weakNetSelecte]){
        DoKitLog(@"yd WeakUpFlow Net");
        [[DoraemonNetworkInterceptor shareInstance].weakDelegate handleWeak:[DoraemonUrlUtil getHttpBodyFromRequest:self.request] isDown:NO];
        [self.task resume];
    }

流量限速大体的类图如下: 图解:
DoraemonNetworkWeakDelegate是在DoraemonNetworkInterceptor定义的,还是作为一个属性存在,代理指向DoraemonWeakNetworkManager,所以DoraemonNSURLProtocol处理方法,就会用到DoraemonNetworkInterceptor的单例,然后回调到DoraemonWeakNetworkManager的相关代理方法,就可以拿到数据进行处理。

下行流量的限速是在DoraemonNSURLProtocol的didReceiveData方法,当数据到来时,获取数据大小,然后进行数据大小限制,主要运用usleep(500000),进行微妙级非主线程阻塞。

- (BOOL)limitSpeed:(NSData *)data isDown:(BOOL)is{
    BOOL result = NO;
    CGFloat speed = is ? _downFlowSpeed : _upFlowSpeed ;
    if(0 == data.length || data.length < (kbChange(speed) ? : kbChange(2000))){
        [self showWeakNetworkWindow:is speed:speed];
        result = YES;
    }
    else{
        [self showWeakNetworkWindow:is speed:speed];
        usleep(_sleepTime);
        [self showWeakNetworkWindow:is speed:speed];
        usleep(_sleepTime);
    }
    [self flowChange:is change:NO];
    return result;
}

总结:

以上是我对DoKit在网络部分的使用思想的理解,和大部分使用NSURLProtocol一样,都是经过注册—>拦截—>转发—>回调—>结束5大步骤。NSURLProtocol是NSURLConnection的handle类, 它更像一套协议,如果遵守这套协议,网络请求Request都会经过这套协议里面的方法去处理。

注意点:

  1. 回调问题:发现URLConnection能发出请求但回调并不会走,这个很好理解,因为URLConnection的回调默认和发起的线程相同;这个问题的关键在于使URLConnection的回调在一个存活的线程中。尝试在回调返回client前想在异步里处理数据并返回client,结果client无法收到数据。
  2. 一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。WKWebView不起作用,因为WKWebView走得是WebKit内核,不走苹果这一套逻辑。目前团队正在针对WKWebView的网络进行拦截研究。
  3. 对于业务方出现使用AFNetworking、NSURLProtocol发起的网络,DoKit在NSURLSessionConfiguration的load方法里进行hook,保证protocolClasses属性只有DoraemonNSURLProtocol,保证了网络能正常拦截。
  4. 如果其中一个Protocol的canInitWithRequest方法返回了YES,则后续的Protocol不再执行;否则会一直遍历,直到找到能处理此请求的Protocol。DoKit只有一个NSURLProtocol的子类,所有很好的避免了这个问题。
  5. 对于每个拦截下来的网络请求,要对其进行标记,不然就会引起死循环,导致网络拦截失败。