前语

之前在滴滴的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看起来像是一个协议,但其实这是一个抽象类,我们要运用它的时分需求创立它的一个子类,并且需求被注册。

DoKit中网络拦截的使用总结
从上图中我们能够看出在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];

DoKit中网络拦截的使用总结
图解:
每一个想进行网络阻拦的功用都得先完成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赋予该署理目标。如图

DoKit中网络拦截的使用总结
图解:
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的处理。

DoKit中网络拦截的使用总结
图解:
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];
    }

流量限速大体的类图如下:

DoKit中网络拦截的使用总结
图解:
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. 关于每个阻拦下来的网络恳求,要对其进行标记,不然就会引起死循环,导致网络阻拦失败。