很多时候会出现这种场景,有两个网络请求A和B,B需要在A访问完毕之后才能进行访问。也就是说,网络访问请求需要有一定顺序,即产生一定依赖关系。

请求A --> A返回值 --> 请求B --> B返回值

解决这个问题,第一时间想到的是NSOperation,毕竟AFNetworking 2.0时代所有的网络访问都是基于NSOperation,想到它自然而然。我们试图利用NSOperation自身提供的addDependency来添加这个依赖。但实际上这种方法并不能保证两个请求是按序进行的,这种方式只能保证请求A是在请求B发出之前发出,但是不能保证A返回值的未知,有可能出现以下情况是我们不希望看到的。

请求A --> 请求B --> A返回值 --> B返回值
或
请求A --> 请求B --> B返回值 --> A返回值

还有另外一个解决办法,就是同步请求。我们常见的网络访问都是用的异步访问,这样可以防止在网络访问的时候卡死UI。这里所说的同步请求,是指在一个新开线程中进行同步请求,譬如,我们可以GCD创建一个Serial Dispatch Queue,将所有的需要按序请求的访问放在同一个串行队列里,这样就可以保证达到我们想要的效果。但是这种实现方式有种很大的问题,通常情况下,现有的网络访问几乎都是异步的,譬如在AFNetworking 3.0中就压根没有提供同步访问的方式,为了这个需求而在整个网络层大动干戈显然是不可能的,况且即便把网络访问全部改成同步的,这样以后使用起来也不访问,所以这种方式显然是不可取的。

为解决这个问题,我想到了信号量。对GCD信号量不熟悉的同学可以先看GCD学习笔记<三> —— 信号量这篇文章。其实之前看信号量这部分的时候,大多是纯理论的,很少有实践的机会,这次正好实践一番。
这个其实是生产-消费者问题的简化版本,这里生产只要生产一次就就可以了,而消费者没有进行实际的消费,只是看到有生产完成就可以了。

思路大体是这样的,给每个网络访问请求添加一个信号量,默认计数为0,当该请求完毕后,将其计数增长为1。当请求B依赖请求A时,请求B在真正发出去之前,需要先等待A的信号量,只有持有A信号量之后,再去发送请求B,注意这里持有A信号量之后应当立即释放,以不影响其他网络请求。
具体实现如下:
正常情况下,每个工程的网络请求一般都会封装成APIManager,我们在BaseAPIManager中添加一个属性

@property (nonatomic, readonly) dispatch_semaphore_t continueMutex;

在初始化的时候,我们将其初始化为

_continueMutex = dispatch_semaphore_create(0);

在网络访问完毕时候进行signal

dispatch_semaphore_signal(self.continueMutex);

此时这里相当于生产-消费问题中的生产者,只不过其比较特殊,它只需要生产一次就够用了,当然多了也无所谓。

再添加一个私有属性来管理APIManager的依赖关系

@property (nonatomic, strong) NSMutableSet *dependencySet;

注意此处用NSMutableSet而没用NSHash​Table,是由于此处必须是强引用,以防止在此apiManager请求前,所依赖的apiManager被释放,而导致无法判断依赖的apiManager是否完成,这样以来,由于依赖关系的存在,会导致被依赖的apiManager只能等待所有产生该以该apiManager为前提条件的apiManager被释放完后才能释放。如果利用不当,这里会有内存泄露的问题。

类比NSOperation,我们也为BaseAPIManager添加如下两个方法

- (void)addDependency:(YLBaseAPIManager *)apiManager;
- (void)removeDependency:(YLBaseAPIManager *)apiManager;

- (void)addDependency:(YLBaseAPIManager *)apiManager {
    [self.dependencySet addObject:apiManager];
}

- (void)removeDependency:(YLBaseAPIManager *)apiManager {
    [self.dependencySet removeObject:apiManager];
}

我们在用户发起网络访问的时候实现如下

[self waitForDependency:^{
   // 在此真正发起网络访问请求
}];

waitForDependency的实现如下:

- (void)waitForDependency:(dispatch_block_t)block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        for (YLBaseAPIManager *apiManager in self.dependencySet) {
            NSLog(@"wait %@",apiManager);
            dispatch_semaphore_wait(apiManager.continueMutex, DISPATCH_TIME_FOREVER);
            // 得到后立刻释放,防止其他请求无法进行
            NSLog(@"%@ Done",apiManager);
            dispatch_semaphore_signal(apiManager.continueMutex);
        }
        if(block) {
            block();
        }
    });
}

在这个方法里,会等待所有依赖的apiManager访问完毕。这里其实相当于生产-消费者问题中的消费者,但是这个消费者有点特殊之处在于,它其实不进行消费,它只是看着。在等到continueMutex信号量之后,又立刻释放该信号量,因为此时目的已经达到,我们已经知晓该被依赖的apiManager已经请求完毕了。

到此,我们需要的依赖关系已经建立完成了,为了描述方便,这里没有考虑线程安全的问题,dependencySet其实是线程不安全的,这个真正使用的时候还需要加锁来保证线程安全。
现在我们可以愉快的按照我们想要的顺序,给对应apiManager添加依赖关系了。但是一定要注意的是,一旦有了依赖关系,使用不当就会造成死锁,这个一定需要注意。

除了利用GCD的信号量,也有另外一个中心化的思路,就是建立一个全局的Manager,所有的网络请求都要向它申请,所有网络请求完毕也需要告知这个Manager。 每个网络访问发起时,便先查看其依赖请求是否完毕,如果没有则不发起,将请求放到缓冲池中。每当有请求发起完毕时,便通知该Manager,然后Manager再去查找是否有依赖这个请求的请求,如果有,则将其拿出来把请求发送出去。当然的缺点在于这一下子让本来分散的网络请求中心化了,这样会浪费很多不必要的注册、查找。而且还需要处理好当所有依赖的网络请求请求完毕后,及时释放被依赖的网络请求。

利用这个依赖关系,我们还可以做更多有意思的事情,譬如我们可以对BaseAPIManager建立一个针对所有APIManager的依赖关系,让所有APIManager都依赖于某个指定网络请求,譬如Token刷新的请求,这样就相当于以AOP的形式,用户无感知地对过期Token进行更新后重新发起该网络请求,当然这还需要额外的逻辑需要在网络访问结束后,如果得到改token已过期的信息,先通知TokenRefresher需要进行刷新,然后重新发起该请求即可。由于TokenRefresher应该是一个单例,在其请求前,先dispatch_semaphore_wait(self.continueMutex, DISPATCH_TIME_FOREVER);持有自己的信号量,这样会导致信号量计数为0,所以其他依赖TokenRefresher的apiManager就需要等它更新完后才会发起请求了。

这个在这里就不细说了,本文涉及到的完整Demo(包含最后所说的TokenRefresher)都包含在这里了YLNetworking,有兴趣的同学可以自己去看下。
当然也应该有很多不足的地方,欢迎指正。