编程一段时间后就喜欢整一些类似“解耦”,“框架”等高大上的词汇糊弄人,原谅我的卖弄。
说起解耦,其实说白了,就是大家各自干各自的事情,相互不干扰。放到软件开发中就是【展示层】---【逻辑业务层】----【数据层】不相互干扰。只要接触过iOS开发的人都知道AFNetworking这个网络请求库,它封装了iOS自身的NSURLSession或者NSURLConnection,提供了各种各样的网络请求功能。正式的开发中是业务层需要请求服务器获取数据并刷新UI,更新到展示层(也就是高大上的“渲染”一词)。在请求的时候只要指明请求的URL和参数即可,然后在规定的地方拿到回调。可能大家觉得其实这已经很“解耦”了,有必要自己再写套框架来多此一举嘛,别急着发牢骚,请先回答我的几个问题:

1.多个页面调用同一个网络请求,我是否每个页面都要写一遍;某个网络请求改了数据返回类型,我是否需要每个涉及到这个请求的页面都要重新改一遍?
2.开发前期服务端没有走通数据的情况下如何模拟网络请求?
3.一般每个公司都会订一套网络返回数据格式,是否每个页面都需要做一次判断处理(主要是errorcode,errormessage等固定的字段)
4.如何有效的进行单元测试?
5.更改了新的网络三方库怎么办?

如果对于以上的问题你完全没有考虑过或者考虑过但无从下手,那么我只能说兄弟,是时候好好看看我的文章了哈哈哈哈。

其实我的这个框架说简单其实也很简单,我们先纵览一下它的大概流程

      ------------------------------------------------------
     |---------------|                       |
     |  controller   |                |-----------|
     |_______________|                |  Mongo    |
             |                         -----------
             |                               |
             |do request--------------------->|
             |                                |
             |                                |
             | <--------------------call back |
             |                                |

Mogo内部的流程图如下(请忽略每个请求前面的AGJ这三个字母,是我上家公司的缩写,现在为了适应统一化,前缀已改成了MG):

uml_mongo.jpg
其中:
1.NetworkAccess 负责具体网络请求的实施
2.NetworkServiceMediator 负责网络请求的分发
3.TaskPool负责管理网络请求线程池

具体到代码中,我们需要在Controller中调用doService方法,这个方法将网络请求作为一个服务来处理,每个网络请求对应一个服务,使用宏定义的URL相对路径用于区分每个请求。调用请求后,首先由TaskPool分配线程并通过ServiceMediator分发到具体的网络请求,分发请求也是通过以URL作为Token定位到具体的请求的。
大概的流程知道后,我们就可以开始一步步分析网络请求的整个流程了

TaskPool 管理网络请求线程池

每次调用doService就会创建一个线程,但不会无限创建,上限是5个(当然可以根据你的需要设置成其他数量),做成串行队列用于依次接受网络请求。
taskpool还负责获取网络请求的回调并回传给相应对象,这一步是通过多播代理类TaskPoolDelegateManager实现的。做过XMPP开发的都知道多播代理的好处,它能管理代理本身(其实通知的实现方式也是类似于多播代理的)。TaskPoolDelegateManager管理的TaskPoolDelegate的属性delegate是这么申明的:

@property (nonatomic, weak) id delegate;

通过weak关键字可以避免忘记释放对象引起的内存泄露。当然,不用多播代理也是可以的,这里使用多播代理的另外一个原因是要管理每个请求的回调,确保A发出的a请求,b请求,c请求回调后能对应到对应的a回调,b回调,c回调。实现起来也并不难,代码如下:

//添加到token
NSMutableArray *requestTokens = [[NSMutableArray alloc] initWithArray:self.requestTokens];
//获取对象的唯一辨识,目前没什么好方法,暂时用hash
NSString *resultServiceName = [NSString stringWithFormat:@"%lu.%@.%@",(unsigned long)self,serviceName,params];
[requestTokens addObject:resultServiceName];

以及

for (NSString *requestToken in self.requestTokens) {
    NSString *resultServiceName = [NSString stringWithFormat:@"%lu.%@.%@",(unsigned long)self,service.serviceName,((MGNetwokResponse *)response).requestParams];
    NSLog(@"requestToken:%@",requestToken);
    NSLog(@"resultServiceName:%@",resultServiceName);
    
    if ([requestToken isEqualToString:resultServiceName]) {
        [self serviceName:service.serviceName response:response];
        NSMutableArray *tempTokens = [NSMutableArray arrayWithArray:self.requestTokens];
        [tempTokens removeObject:resultServiceName];
        self.requestTokens = tempTokens;
    }
}

这两段代码,第一段是加token,用于区分每个网络请求,第二段是去除请求的对象对应的token。那么,这个token就是用于区分同一个对象的不同请求

ServiceMediator 分发网络请求

Mediator,中介者,因为是Operation的子类,因此每个Mediator被加到TaskPool后会自动执行main函数,main函数中调用一些预先定义的接口供下层调用。需要注意的是,TaskPool不会具体的去调用具体的ServiceMediator,这样就失去了框架的扩展性。这里的处理方式是,TaskPool会留一个注册ServiceMediator的方法,这个方法就是指定ServiceMediator的某个子类来做具体的NetworkAccess请求。
在ServiceMediator中有如下代码:

NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:service];
    
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:service];
    
[invocation retainArguments];

这么做是为了调用相应的方法,弥补performSelector:withObject:等函数的不足

NetworkAccess 具体网络请求实现

一个典型的网络请求是这样的:
(1)通过方法
-(instancetype)initWithHost:(NSString *) host modulePath:(NSString *) path;
初始化,其中,host是地址,path是资源路径
(2)通过
-(MGNetwokResponse *)doHttpRequest:(NSString *)requestUrlString params:(NSDictionary *) params;
做具体的网络请求。

iOS默认的网络请求是异步的,但NetworkAccess是需要阻塞运行的,因为NetworkAccess本身已经被丢进线程中执行了。因此NetworkAccess内部是通过信号量将异步请求转换成同步请求。
信号量各位在iOS开发中可能使用的不多,但确实是多线程开发中特别好用的技巧之一:
第一步:通过dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 创建一个信号量
当网络请求发出,
第二步:需要等后台返回数据时调用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);方法等待接受收据。等数据接受完毕后,
第三步:调用dispatch_semaphore_signal(semaphore);代码将恢复到第二步后执行。

以上就是Mongo的请求流程了,有问题的还请赐教

github地址:https://github.com/zjh171/Mongo.git


最后解答一下文章开头的几个问题^_^

1.模拟网络请求
也就是Mock请求,对应的类是MGMockAccess,我们只需要在NetworkServiceMediator中将请求的HOST地址改为HOST_MOCK即可,具体的看里面的Demo吧
2.单元测试
新建一个ServiceMediator并调用start命令即可,因为ServiceMediator是一个线程
3.缺省的json数据处理
对应的类是MGJsonHandler,处理后丢给对象MGNetwokResponse。其中MGNetwokResponse中的rawJson是原始数据,errorCode是错误码,errorMessage是错误消息,responseObj是过滤后的对象,可以是数组或字典。