内容简介:GYHttpMock是腾讯团队开源的用于模拟网络请求的工具。截获指定的http Request,返回我们自定义的response。本文意在解析其细节和原理。客户端开发过程中,经常会遇到等服务端联调的情景,往往这个时候我们什么都做不了,这个工具可以轻松解决这个问题。只需要引入工程添加request限制条件,并制定返回json即可。api用的DSL的形式,不懂得可以看这
GYHttpMock是腾讯团队开源的用于模拟网络请求的工具。截获指定的http Request,返回我们自定义的response。本文意在解析其细节和原理。
作用
客户端开发过程中,经常会遇到等服务端联调的情景,往往这个时候我们什么都做不了,这个 工具 可以轻松解决这个问题。只需要引入工程添加request限制条件,并制定返回json即可。
用法
api用的DSL的形式,不懂得可以看这 《objective-c DSL的实现思路》 关于用法官方都有写 《GYHttpMock:iOS HTTP请求模拟工具》 粘过来的 ̄□ ̄|| 1.创建一个最简单的 mockRequest。截获应用中访问www.weread.com 的 get 请求,并返回一个 response body为空的数据。
mockRequest(@"GET", @"http://www.weread.com"); 复制代码
2.创建一个拦截条件更复杂的 mockRequest。截获应用中 url 包含weread.com,而且包含了 name=abc 的参数
mockRequest(@"GET", @"(.*?)weread.com(.*?)".regex).
withBody(@"{\"name\":\"abc\"}".regex);
复制代码
3.创建一个指定返回数据的 mockRequest。withBody的值也可以是某个 xxx.json 文件,不过这个 json 文件需要加入到项目中。
mockRequest(@"POST", @"http://www.weread.com").
withBody(@"{\"name\":\"abc\"}".regex);
andReturn(200).
withBody(@"{\"key\":\"value\"}");
复制代码
4.创建一个修改部分返回数据的 mockRequest。这里会根据 weread.json 的内容修改正常网络返回的数据
mockRequest(@"POST", @"http://www.weread.com").
isUpdatePartResponseBody(YES).
withBody(@"{\"name\":\"abc\"}".regex);
andReturn(200).
withBody(@“weread.json");
复制代码
假设正常网络返回的原始数据是这样:
{"data": [ {
"bookId":"0000001",
"updated": [
{
"chapterIdx": 1,
"title": "序言",
},
{
"chapterIdx": 2,
"title": "第2章",
}
]
}]}
复制代码
weread.json的内容是这样
{"data": [{
"updated": [
{
"hello":"world"
}
]
}]}
复制代码
修改后的数据就会就成这样:
{"data": [ {
"bookId":"0000001",
"updated": [
{
"chapterIdx": 1,
"title": "序言",
"hello":"world"
},
{
"chapterIdx": 2,
"title": "第2章",
"hello":"world"
}
]
}]}
复制代码
实现原理
流程
NSURLProtocol
一个不恰当的比喻,NSURLProtocol就好比一个城门守卫,请求就相当于想进城买东西的平民,平民从老家来想进城,这时城门守卫自己做起了生意,看到有漂亮姑娘就直接把东西卖给她省的她进城了,小伙子就让他进城自己去买。等这些人买到东西回村儿,村里人看见他们买到了东西很高兴,但是并不知道这个东西的来源。
首先NSURLProtocol是一个类,并不是一个协议。我们要使用它的时候必须要创建其子类。它在IOS系统中处于这样一个位置:
在IOS 的URL Loading System中的网络请求它都可以拦截到,IOS在整个系统设计上的强大之处。 贴一张URL Loading System的图
也就是说常用的NSURLSession、NSURLConnection及UIWebView都可以拦截到,但是WKWebView走的不是IOS系统的网络库,并不能拦截到。
GYHttpMock也正是基于NSURLProtocol实现的,NSURLProtocol主要分为5个步骤: 注册——>拦截——>转发——>回调——>结束
注册:
调用NSURLProtocol的工厂方法,但是GYHttpMock用了更巧妙的方式,待会再说。注册之后URL Loading System的request都会经过myURLProtocol了。
[NSURLProtocol registerClass:[myURLProtocol class]]; 复制代码
拦截:
判断是否对该请求进行拦截。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request 复制代码
在该方法中,我们可以对request进行处理。例如修改头部信息等。最后返回一个处理后的request实例。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 复制代码
转发:
核心方法将处理后的request重新发送出去。这里完全由自己定义,可以直接返回本地的数据,可以对请求进行重定位等等。
- (void)startLoading {
复制代码
回调:
因为是面向切面编程,所以不能影响到原来的网络逻辑。需要将处理后返回的数据发送给原来网络请求的地方。
[self.client URLProtocol:self didFailWithError:error]; [self.client URLProtocolDidFinishLoading:self]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [self.client URLProtocol:self didLoadData:data]; 复制代码
这里self.client 是URLProtocol的一个属性,是客户端的一个抽象类。通过调用其方法可以把数据回调给网络请求的地方。
结束:
//请求完全结束的时候,会调用
- (void)stopLoading 复制代码
应用
URLProtocol功能非常强大,可以用作请求缓存、网络请求mock正如GYHttpMock、网络相关数据统计、URL重定向。等等。。。
##源码解析 ####文件结构
源码并不多,文件结构也很简单。
-
GYMockURLProtocol:请求拦截核心代码。 -
GYMatcher:工具类,比较数据等同性。 *GYHttpMock:单例,起调配作用,保存待拦截request、注册网络相关类的hook、等。 -
Response:对httpResponse的描述和抽象,还有DSL相关类。 -
Request:对httpRequest的描述和抽象,还有DSL相关类。 -
Hooks:对网络配置相关类的hook(就是钩子的意思,通常是用Swizzle替换系统方法或在系统方法中插入代码来实现某种功能),通过hook相关方法注册GYMockURLProtocol类,使Request拦截生效。 -
Categories:对NSString和request的扩展,方便使用
看下源码的调用过程。
mockRequest(@"GET", @"(.*?)feed/setting(.*?)".regex).
andReturn(200).
withBody(@"test.json");
复制代码
在 mockRequest 中 GYMockRequest 并不是继承自 NSURLRequest ,它是对request的描述, GYMockRequestDSL 起到了链式编程中传值的作用,用且block代替方法(block和方法返回的都是 GYMockRequestDSL 对象)。在方法中创建的request被分别保存在 GYMockRequestDSL (用作GYMockRequest赋值)中和 GYHttpMock (用作拦截请求)中。
GYMockRequestDSL *mockRequest(NSString *method, id url) {
GYMockRequest *request = [[GYMockRequest alloc] initWithMethod:method urlMatcher:[GYMatcher GYMatcherWithObject:url]];
GYMockRequestDSL *dsl = [[GYMockRequestDSL alloc] initWithRequest:request];
[[GYHttpMock sharedInstance] addMockRequest:request];
[[GYHttpMock sharedInstance] startMock];
return dsl;
}
复制代码
GYHttpMock 维护着如下两个数组, addMockRequest 方法会将request保存在 stubbedRequests 中保存,
//保存的request @property (nonatomic, strong) NSMutableArray *stubbedRequests; //需要hock的类,存储类对象 @property (nonatomic, strong) NSMutableArray *hooks; 复制代码
并且在初始化时候判断需要hook的类,此处因为低版本中无 NSURLSession 类型,所以添加次判断
- (id)init
{
self = [super init];
if (self) {
//初始化数据
_stubbedRequests = [NSMutableArray array];
_hooks = [NSMutableArray array];
//注册URLConnectionHook,
[self registerHook:[[GYNSURLConnectionHook alloc] init]];
if (NSClassFromString(@"NSURLSession") != nil) {
//判断是否有NSURLSession,如果有的则一样注册
[self registerHook:[[GYNSURLSessionHook alloc] init]];
}
}
return self;
}
复制代码
startMock 方法开启网络相关类的hook,这里我们以 GYNSURLSessionHook 为例
//GYHttpMock.m
- (void)startMock
{
if (!self.isStarted){
[self loadHooks];
self.started = YES;
}
}
- (void)loadHooks {
@synchronized(_hooks) {
for (GYHttpClientHook *hook in _hooks) {
//load hock
[hook load];
}
}
}
复制代码
GYNSURLSessionHook 中是hook的 NSURLSessionConfiguration protocolClasses的get方法,它的作用是返回URL会话支持的公共网络协议,作用跟 [NSURLProtocol registerClass:[myURLProtocol class]] 一样,目的是使我们自定义的NSURLProtocol生效,且不用使用者添加注册子类的代码。
//GYNSURLSessionHook.m
@implementation GYNSURLSessionHook
- (void)load {
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
//修改NSURLSessionConfiguration中protocolClasses的返回,
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)unload {
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 NSURLSession hook."];
}
method_exchangeImplementations(originalMethod, stubMethod);
}
//更改URL会话支持的公共网络协议
- (NSArray *)protocolClasses {
return @[[GYMockURLProtocol class]];
}
@end
复制代码
mockRequest(@"GET", @"(.*?)feed/setting(.*?)".regex) 方法完成了 GYMockURLProtocol 的注册,并且把筛选条件(@"get",@"(. ?)feed/setting(. ?)".regex)都存储到 GYMockRequest 中了。 接下来的 andReturn(200).withBody(@"test.json"); 一样是通过中间类传入响应数据,生成 GYMockResponse 的对象,并保存在了对应的 GYMockResponse 对象中,不再赘述,接下来看下拦截请求的代码。
//GYMockURLProtocol.m
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
[[GYHttpMock sharedInstance] log:@"mock request: %@", request];
//根据request判断是否可以发送网络请求
GYMockResponse* stubbedResponse = [[GYHttpMock sharedInstance] responseForRequest:(id<GYHTTPRequest>)request];
if (stubbedResponse && !stubbedResponse.shouldNotMockAgain) {
return YES;
}
return NO;
}
//GYHttpMock.m
//获取request对应的respond
- (GYMockResponse *)responseForRequest:(id<GYHTTPRequest>)request
{
@synchronized(_stubbedRequests) {
for(GYMockRequest *someStubbedRequest in _stubbedRequests) {
if ([someStubbedRequest matchesRequest:request]) {
someStubbedRequest.response.isUpdatePartResponseBody = someStubbedRequest.isUpdatePartResponseBody;
return someStubbedRequest.response;
}
}
return nil;
}
}
复制代码
判断发送的request是否需要拦截,如果在 _stubbedRequests 中可以匹配到,则返回我们自定义的response,并拦截,否则不拦截。 接下来是重中之重,开始请求
- (void)startLoading {
NSURLRequest* request = [self request];
id<NSURLProtocolClient> client = [self client];
GYMockResponse* stubbedResponse = [[GYHttpMock sharedInstance] responseForRequest:(id<GYHTTPRequest>)request];
if (stubbedResponse.shouldFail) {
[client URLProtocol:self didFailWithError:stubbedResponse.error];
}
else if (stubbedResponse.isUpdatePartResponseBody) {
stubbedResponse.shouldNotMockAgain = YES;
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
if (error) {
NSLog(@"Httperror:%@%@", error.localizedDescription,@(error.code));
[client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}else{
id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:json];
if (!error && json) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:stubbedResponse.body options:NSJSONReadingMutableContainers error:nil];
[self addEntriesFromDictionary:dict to:result];
}
NSData *combinedData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:combinedData];
[client URLProtocolDidFinishLoading:self];
}
stubbedResponse.shouldNotMockAgain = NO;
}];
}
else {
NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:stubbedResponse.statusCode HTTPVersion:@"1.1" headerFields:stubbedResponse.headers];
if (stubbedResponse.statusCode < 300 || stubbedResponse.statusCode > 399
|| stubbedResponse.statusCode == 304 || stubbedResponse.statusCode == 305 ) {
NSData *body = stubbedResponse.body;
[client URLProtocol:self didReceiveResponse:urlResponse
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:body];
[client URLProtocolDidFinishLoading:self];
} else {
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
[cookieStorage setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:stubbedResponse.headers forURL:request.URL]
forURL:request.URL mainDocumentURL:request.URL];
NSURL *newURL = [NSURL URLWithString:[stubbedResponse.headers objectForKey:@"Location"] relativeToURL:request.URL];
NSMutableURLRequest *redirectRequest = [NSMutableURLRequest requestWithURL:newURL];
[redirectRequest setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:[cookieStorage cookiesForURL:newURL]]];
[client URLProtocol:self
wasRedirectedToRequest:redirectRequest
redirectResponse:urlResponse];
// According to: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m.html
// needs to abort the original request
[client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}
}
}
复制代码
这里的逻辑是,判断我们对response是部分修改还是全部修改,
- 部分修改:发出网络请求,并根据预设的response要求修改返回值,通过client返回给原来请求的位置。
- 全部修改:直接根据预设的条件创建response,并通过client返回给原来请求的位置。
最后
总体的流程是这样的,当然还有细节值得推敲,大神们如果发现问题,还请及时指出哦!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Processing编程学习指南(原书第2版)
[美]丹尼尔希夫曼(Daniel Shiffman) / 李存 / 机械工业出版社 / 2017-3-1 / 99.00元
在视觉化界面中学习电脑编程的基本原理! 本书介绍了编程的基本原理,涵盖了创建最前沿的图形应用程序(例如互动艺术、实时视频处理和数据可视化)所需要的基础知识。作为一本实验风格的手册,本书精心挑选了部分高级技术进行详尽解释,可以让图形和网页设计师、艺术家及平面设计师快速熟悉Processing编程环境。 从算法设计到数据可视化,从计算机视觉到3D图形,在有趣的互动视觉媒体和创意编程的背景之......一起来看看 《Processing编程学习指南(原书第2版)》 这本书的介绍吧!