前语
之前在滴滴的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。
- 支撑原生接口数据作为Mock模板 阻拦App端原生接口的数据恳求回来成果,支撑上传该数据到我们的渠道端,让我们能够更高效地获取Mock数据模版。
- 支撑多场景成果切换 关于同一个接口数据Mock,支撑不同的场景的成果回来,各个场景灵活切换操控。
- 有用场景丰富 开发前无需等待后端同学开发完成,即可运用接口进行开发;开发中能够结构各种场景的数据,进步提测质量;开发后测试同学能够结构各种反常数据,复现问题更简便,更高效的回归各种场景,更高效的进步研制流程。
2.1 运用:
- 去DoKit渠道获取一个产品ID,然后将DoKit的接入办法
[[DoraemonManager shareInstance] install];
换成带有详细productID:(例)749a060b5e48dd77cfee680be7b1b7
[[DoraemonManager shareInstance] installWithPid:@"productID"];
- 点击终端DoKit面板中的数据Mock功用,看到渠道产品下的场景,详细操作请查看DoKit渠道的运用中心。
- 在终端切换场景,然后加载网络恳求,就会发现终端的响应数据是对应场景下的自界说数据,避免了和后端人员的口舌之交,最重要的是没有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都会经过这套协议里边的办法去处理。
注意点:
- 回调问题:发现URLConnection能宣布恳求但回调并不会走,这个很好理解,由于URLConnection的回调默许和发起的线程相同;这个问题的关键在于使URLConnection的回调在一个存活的线程中。测验在回调回来client前想在异步里处理数据并回来client,成果client无法收到数据。
- 一些时代比较长远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是根据CFNetwork的,所以这些网络库的网络恳求无法被NSURLProtocol阻拦。WKWebView不起作用,由于WKWebView走得是WebKit内核,不走苹果这一套逻辑。现在团队正在针对WKWebView的网络进行阻拦研究。
- 关于业务方出现运用AFNetworking、NSURLProtocol发起的网络,DoKit在NSURLSessionConfiguration的load办法里进行hook,确保protocolClasses特点只要DoraemonNSURLProtocol,确保了网络能正常阻拦。
- 假如其间一个Protocol的canInitWithRequest办法回来了YES,则后续的Protocol不再履行;否则会一向遍历,直到找到能处理此恳求的Protocol。DoKit只要一个NSURLProtocol的子类,一切很好的避免了这个问题。
- 关于每个阻拦下来的网络恳求,要对其进行标记,不然就会引起死循环,导致网络阻拦失败。