作为一名iOS开发工作者,大家应该都听过YTKNetwork框架吧。它是猿题库技术团队开源的一个网络请求框架,内部封装了AFNetworking。它把每个请求实例化,管理它的生命周期,也可以管理多个请求。
在正式讲解源码之前,我会先讲一下该框架所用的架构和设计模式。我总觉得对架构和设计有一定的了解的话,会有助于对源码的理解。
1. 架构
先上图:
在这里简单说明一下:
- YTKNetwork框架将每一个请求实例化,YTKBaseRequest是所有请求类的基类,YTKRequest是它的子类。所以如果我们想要发送一个请求,则需要创建并实例化一个继承于YTKRequest的自定义的请求类(CustomRequest)并发送请求。
- YTKNetworkAgent是一个单例,负责管理所有的请求类(例如CustomRequest)。当CustomRequest发送请求以后,会把自己放在YTKNetworkAgent持有的一个字典里,让其管理自己。
- 我们说YTKNetwork封装了AFNetworking,实际上是YTKNetworkAgent封装了AFNetworking,由它负责AFNetworking请求的发送和AFNetworking的回调处理。所以如果我们想更换一个第三方网络请求库,就可以在这里更换一下。而YTKRequest更多的是只是负责缓存的处理。
- YTKNetworkConfig与YTKPriviate的具体职能现在不做介绍,会在后文给出。
OK,现在我们知道了YTKNetwork中类与类之间的关系以及关键类的大致职能,接下来我会告诉你YTKNetwork为什么会采用这种关系来架构,以及采用这种架构会有什么好处。
2. 设计模式
YTKNetwork框架采用的设计模式是命令模式(Command Pattern)。
首先看一下命令模式的定义:
命令模式将请求封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
摘自:《Head First 设计模式》
看一下命令模式的类图:
图中英文的含义:
英文 | 中文 |
---|---|
Command | 抽象命令类 |
ConcreteCommand | 命令类的实现类(子类) |
Invoker | 调用者 |
Receiver | 命令接收者(执行者) |
Client | 客户端 |
详细介绍一下:
- 命令模式的本质是对命令的封装,将发出命令的责任和执行命令的责任分割开。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
可能还是觉得有点抽象,在这里举一个《Head First 设计模式》里的例子,一个客人在餐厅点餐的过程:
- 你将点的菜写在订单里,交给了服务员。
- 服务员将订单交给厨师。
- 厨师做好菜之后将做好的菜交给服务员。
- 最后服务员把菜递给你。
在这里,命令就好比是订单,而你是命令的发起者。你的命令(订单)通过服务员(调用者)交给了命令的执行者(厨师)。
所以至于这道菜具体是谁做,怎么做,你是不知道的,你做的只是发出命令和接受结果。而且对于餐厅来说,厨师是可以随便换的,而你可能对此一无所知。反过来,厨师只需要好好把菜做好,至于是谁点的菜也不需要他考虑。
结合上面命令模式的类图以及餐厅点餐的例子,我们来理清一下YTKNetwork
内部的职能
场景 | Command | ConcreteCommand | Invoker | Receiver | Client |
---|---|---|---|---|---|
餐厅 | 空白订单 | 填入菜名的订单 | 服务员 | 厨师 | 客人 |
YTKNetwork | YTKBaseRequest | CustomRequest | YTKNetworkAgent | AFNetworking | ViewController/ViewModel |
可以看到,YTKNetwork对命令模式的实现是很符合其设计标准的,它将请求的发起者和接收者分离开来(中间隔着调用者),可以让我们随时更换接受者。
另外,因为封装了请求,我们既可以管理单个请求,也可以同时管理多个请求,甚至实现琏式请求的发送。关于多个请求的发送,我们也可以想象在餐厅里,你可以在吃的过程中还想起来要吃别的东西,例如点心,饮料之类的,你就可以填多个订单(当然也可以写在一起)交给服务员。
相信到这里,大家应该对YTKNetwork的设计与架构有了足够的认识了,下面进入到真正的源码解析,我们结合一下它的代码来看一下YTKNetwork是如何实现和管理网络请求的。
3. 源码解析
在真正讲解源码之前,我先详细说一下各个类的职责:
3.1 责任介绍
类名 | 职责 |
---|---|
YTKBaseRequest | 所有请求类的基类。持有NSURLSessionTask实例,responseData,responseObject,error等重要数据,提供一些需要子类实现的与网络请求相关的方法,处理回调的代理和block,命令YTKNetworkAgent发起网络请求。 |
YTKRequest | YTKBaseRequest的子类。负责缓存的处理:请求前查询缓存;请求后写入缓存。 |
YTKNetworkConfig | 被YTKRequest和YTKNetworkAgent访问。负责所有请求的全局配置,例如baseUrl和CDNUrl等等。 |
YTKNetworkPrivate | 提供JSON验证,appVersion等辅助性的方法;给YTKBaseRequest增加一些分类。 |
YTKNetworkAgent | 真正发起请求的类。负责发起请求,结束请求,并持有一个字典来存储正在执行的请求。 |
YTKBatchRequest | 可以发起批量请求,持有一个数组来保存所有的请求类。在请求执行后遍历这个数组来发起请求,如果其中有一个请求返回失败,则认定本组请求失败。 |
YTKBatchRequestAgent | 负责管理多个YTKBatchRequest实例,持有一个数组来保存YTKBatchRequest。支持添加和删除YTKBatchRequest实例。 |
YTKChainRequest | 可以发起链式请求,持有一个数组来保存所有的请求类。当某个请求结束后才能发起下一个请求,如果其中有一个请求返回失败,则认定本请求链失败。 |
YTKChainRequestAgent | 负责管理多个YTKChainRequestAgent实例,持有一个数组来保存YTKChainRequest。支持添加和删除YTKChainRequest实例。 |
OK,现在知道了YTKNetwork内部的责任分配,下面我们先从单个请求的全部流程(配置,发起,结束)来看一下YTKNetwork都做了什么。
3.2 单个请求
3.21 单个请求的配置
官方的教程建议我们将请求的全局配置是在AppDelegate.m文件里,设定baseUrl以及cdnUrl等参数。
1 | - (BOOL)application:(UIApplication *)application |
如果我们需要新建一个注册的请求,则需要创建一个继承于YTKRequest的注册接口的类RegisterApi,并将针对该请求参数配置好:
1 | // RegisterApi.h |
现在我们知道如何配置全局的参数和针对某个请求的参数了,接下来看一下单个请求是如何发起的。
3.22 单个请求的发起
还是刚才的注册API,在实例化以后,直接调用startWithCompletionBlockWithSuccess:failure
方法(或start
方法)就可以发起它:
1 | //LoginViewController.m |
上面是以block的形式回调,YTKNetwork也支持代理的回调:
1 | //LoginViewController.m |
有两点需要注意的是:
- 必须给自定义请求类(RegisterApi)调用
startWithCompletionBlockWithSuccess:failure
方法(或start
方法),才能真正发起请求。- 在同时设置了回调代理和回调block的情况下,首先回调的是回调代理方法,然后再走回调block。
知道了YTKRequest请求是如何在外部发起的,我们现在从startWithCompletionBlockWithSuccess:failure
方法开始,来看一下YTKNetwork都做了什么:
首先来到YTKBaseRequest类(因为最早是由它定义的该方法):
1 | //YTKBaseRequest.m |
当保存完成功和失败的block以后,调用start
方法,于是来到了YTKRequest类(注意,虽然YTKBaseRequest也实现了start
方法,但是由于YTKRequest类是它的子类并也实现了start方法,所以这里最先走的是YTKRequest类的start
方法):
1 | //YTKRequest.m |
我们之前说过YTKRequest负责缓存的相关处理,所以在上面这个start方法里,它做的是请求之前缓存的查询和检查工作:
- 如果忽略缓存,或者缓存获取失败,调用startWithoutCache方法(参考1-3的情况),发起请求。
- 如果能成功获取到缓存,则直接回调(参考4-7的情况)。
我们来看一下每一步的具体实现:
ignoreCache
属性是用户手动设置的,如果用户强制忽略缓存,则无论是否缓存是否存在,直接发送请求。resumableDownloadPath
是断点下载路径,如果该路径不为空,说明有未完成的下载任务,则直接发送请求继续下载。loadCacheWithError:
方法验证了加载缓存是否成功的方法(返回值为YES,说明可以加载缓存;反之亦然),看一下具体实现:
1 | //YTKRequest.m |
先讲一下什么是元数据:元数据是指数据的数据,在这里描述了缓存数据本身的一些特征:包括版本号,缓存时间,敏感信息等等, 稍后会做详细介绍。
我们来看一下上面关于缓存的元数据的获取方法:loadCacheMetadata
方法
1 | //YTKRequest.m |
cacheMetadata(YTKCacheMetadata) 是当前reqeust类用来保存缓存元数据的属性。
YTKCacheMetadata类被定义在YTKRequest.m文件里面:
1 | //YTKRequest.m |
它描述的是缓存的版本号,敏感信息,创建时间,app版本等信息,并支持序列化处理,可以保存在磁盘里。
因此,loadCacheMetadata
方法的目的是将之前被序列化保存的缓存元数据信息反序列化,赋给自身的cacheMetadata
属性上。
现在获取了缓存的元数据并赋给了自身的cacheMetadata
属性上,那么接下来就要逐一验证元数据里的各项信息是否符合要求,在下面的validateCacheWithError:
里面验证:
1 | //YTKRequest.m |
如果每项元数据信息都能通过,再在loadCacheData
方法里面验证缓存是否能被取出来:
1 | //YTKRequest.m |
如果通过了最终的考验,则说明当前请求对应的缓存是符合各项要求并可以被成功取出,也就是可以直接进行回调了。
当确认缓存可以成功取出后,手动设置dataFromCache
属性为 YES,说明当前的请求结果是来自于缓存,而没有通过网络请求。
然后在真正回调之前做了如下处理:
1 | //YTKRequest.m: |
5.1:requestCompletePreprocessor
方法:
1 | //YTKRequest.m: |
1 | //YTKRequest.m: |
我们可以看到,
requestCompletePreprocessor
方法的任务是将响应数据保存起来,也就是做缓存。但是,缓存的保存有两个条件,一个是需要cacheTimeInSeconds
方法返回正整数(缓存时间,单位是秒,后续会详细说明);另一个条件是isDataFromCache
方法返回NO。但是我们知道,如果缓存可用,就会将这个属性设置为YES,所以走到这里的时候,就不做缓存了。
接着看下5.2:requestCompleteFilter
方法则是需要用户自己提供具体实现的,专门作为回调成功之前的一些处理:
1 | //YTKBaseRequest.m |
到这里,回调之前的处理都结束了,下面来看一下在缓存可用的情况下的回调:
1 | //YTKRequest.m |
我们可以看到 ,这里面同时存在两种回调:代理的回调和block的回调。先执行的是代理的回调,然后执行的是block的回调。而且在回调结束之后,YTKNetwork会帮助我们清空回调的block:
1 | //YTKBaseRequest.m |
注意,在用户同时实现了代理和block的情况下,二者都会被调用。
到这里,我们了解了YTKNetwork在网络请求之前是如何验证缓存,以及在缓存有效的情况下是如何回调的。
反过来,如果缓存无效(或忽略缓存)时,需要立即请求网络。那么我们现在来看一看在这个时候YTKNetwork都做了什么:
仔细看一下上面的start
方法,我们会发现,如果缓存不满足条件时,会直接调用startWithoutCache
方法:
1 | //YTKRequest.m |
那么在startWithoutCache
方法里都做了什么呢?
1 | //YTKRequest.m |
在这里,首先清除了关于缓存的所有数据,然后调用父类的start
方法:
1 | //YTKBaseRequest.m: |
第一步里的Accessories是一些遵从代理的对象。这个代理定义了一些用来追踪请求状况的方法。它被定义在了YTKBaseRequest.h文件里:
1 | //用来跟踪请求的状态的代理。 |
所以只要某个对象遵从了这个代理,就可以追踪到请求将要开始,将要结束,已经结束的状态。
接着看一下第二步:YTKNetworkAgent把当前的请求对象添加到了自己身上并发送请求。来看一下它的具体实现:
1 | //YTKNetworkAgent.m |
这个方法挺长的,但是请不要被吓到,它总共分为三个部分:
- 第一部分是获取当前请求对应的task并赋给request的requestTask属性(以后提到的request,都为用户自定义的当前请求类的实例)。
- 第二部分是把request放入专门用来保存请求的字典中,key为taskIdentifier。
- 第三部分是启动task。
下面我来依次讲解每个部分:
第一部分:获取当前请求对应的task并赋给request:
1 | //YTKNetworkAgent.m |
在这里判断了用户是否自定义了request:
- 如果是,则直接调用AFNetworking的dataTaskWithRequest:方法。
- 如果不是,则调用YTKRequest自己的生成task的方法。
第一种情况就不说了,因为AF帮我们做好了。在这里看一下第二种情况,sessionTaskForRequest: error :
方法内部:
从这个方法最后的switch语句可以看出,这个方法的作用是返回当前request的NSURLSessionTask的实例。而且最终生成NSURLSessionTask实例的方法都是通过dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:
这个私有方法来实现的。在讲解这个关键的私有方法之前,先来逐步讲解一下这个私有方法需要的每个参数的获取方法:
- 获得请求类型(GET,POST等):
1 | //YTKNetworkAgent.m |
requestMethod
方法最初在YTKBaseRequest里面已经实现了,默认返回了YTKRequestMethodGET。
它的枚举类型在YTKBaseRequest.h里面定义:
1 | //YTKBaseRequest.h |
用户可以根据实际的需求在自定义request类里面重写这个方法:
1 | //RegisterAPI.m |
- 获得请求url:
1 | //YTKNetworkAgent.m |
- 获得请求参数
1 | //YTKNetworkAgent.m |
在这里,requestArgument是一个get方法,需要用户自己定义请求体,例如在RegisterAPI里面就定义了两个请求参数:
1 | //RegisterApi.m |
- 获得request serializer
1 | //YTKNetworkAgent.m |
上面这个方法通过传入的request实例,根据它的一些配置(用户提供)来获取AFHTTPRequestSerializer的实例。
到现在为止,获取NSURLSessionTask实例的几个参数都拿到了,剩下的就是调用dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:
方法来获取NSURLSessionTask实例了。我们来看一下这个方法的具体实现:
1 | //YTKNetworkAgent.m |
这两个方法,上面的方法调用了下面的来获取最终的NSURLSessionDataTask实例。
OK,现在我们已经知道了NSURLSessionDataTask实例是如何获取的,再来看一下在addRequest:方法里接下来做的是对序列化失败的处理:
1 | //YTKNetworkAgent.m |
requestDidFailWithRequest:方法专门处理请求失败的情况,因为它被包含在统一处理请求回调的方法中,所以在稍后会在讲解统一处理请求回调的方法的时候再详细讲解这个方法。
继续往下走,到了优先级的映射部分:
1 | //YTKNetworkAgent.m |
requestPriority是YTKBaseRequest的一个枚举属性,它的枚举在YTKBaseRequest.h里面被定义:
1 | typedef NS_ENUM(NSInteger, YTKRequestPriority) { |
在这里,将用户设置的YTKRequestPriority映射到NSURLSessionTask的priority上。
到这里,我们拿到了task的实例并设置好了优先级,紧接着就是addRequest:方法里的第二个部分:
YTKNetworkAgent将request实例放在了一个字典中,保存起来:
第二部分:把request放入专门用来保存请求的字典中,key为taskIdentifier:
1 | //YTKNetworkAgent.m |
可以看到,在添加前和添加后是进行了加锁和解锁的处理的。而且request实例被保存的时候,将其task的identifier作为key来保存。
在当前的request被保存以后,就到了最后一步,正式发起请求:
第三部分:启动task
1 | //YTKNetworkAgent.m |
到现在为止,我们了解了YTKNetwork里面,一个请求开始之前做的事情:查找可用缓存,生成NSURLSessionTask实例,获取url,requestSerializer,将request放到YTKNetworkAgent的一个字典里等等(详细流程会在稍后给出)。
那么接下来我们看一下YTKNetwork是如何处理请求的回调的。
眼尖的同学们可能会注意到,在获取NSURLSessionTask实例的时候,出现了两次“响应的统一处理”的注释,大家可以搜索这个注释就可以找到这个方法:handleRequestResult:responseObject:error:
。这个方法负责的是对请求回调的处理,当然包括了成功和失败的情况。我们来看一下在这个方法里都做了什么:
1 | //YTKNetworkAgent.m |
简单讲解一下上面的代码:
- 首先通过task的identifier值从YTKNetworkAgent保存的字典里获取对应的请求。
- 然后将获得的responseObject进行处理,将处理后获得的responseObject,responseData和responseString赋值给当前的请求实例request。
- 再根据这些值的获取情况来判断最终回调的成败(改变succeed的值)。
- 最后根据succeed的值来进行成功和失败的回调。
这里先重点介绍一下是如何判断json的有效性的:
1 | //YTKNetworkAgent.m |
在这里,首先,用statusCodeValidator方法判断响应的code是否在正确的范围:
1 | //YTKBaseReqiest.m |
然后再判断json的有效性:
1 | //YTKNetworkUtils.m |
注意,YTKNetworkUtils这个类是在YTKNetworkPirvate里面定义的,YTKNetworkPirvate里面有一些工具类的方法,在后面还会遇到。
在验证返回的JSON数据是否有效以后,就可以进行回调了:
1 | //YTKNetworkAgent.m |
我们先来分别看一下请求成功的处理和失败的处理:
请求成功的处理:
1 | //YTKNetworkAgent.m |
我么可以看到,在请求成功以后,第一个做的是写入缓存,我们来看一下requestCompletePreprocessor
方法的实现:
1 | //YTKRequest.m |
首先看一下写入缓存操作的执行条件:当cacheTimeInSeconds
方法返回大于0并且isDataFromCache
为NO的时候会进行写入缓存。
cacheTimeInSeconds
方法返回的是缓存保存的时间,它最初定义在YTKBaseRquest里面,默认返回是-1:
1 | //YTKBaseRequest.m |
所以说YTKNetwork默认是不进行缓存的,如果用户需要做缓存,则需要在自定义的request类里面返回一个大于0的整数,这个整数的单位是秒。
isDataFromCache
属性在上面讲解发送请求部分里的查询缓存的步骤里有介绍。在这里再强调一下:isDataFromCache
的默认值是NO。在请求发起之前,
查询缓存的时候:
- 如果发现缓存不可用(或忽略缓存),则立即发送请求,这个时候,isDataFromCache的值不做更改,仍然是NO。
- 如果发现缓存可用(在不忽略缓存的情况下),就要将isDataFromCache属性设置为YES,说明将不需要发送请求,直接在里获取数据了。
即是说,如果发送了请求,则isDataFromCache
一定是NO的,那么在上面这个判断里面,(!isDataFromCache)就一定为YES了。
因此,如果用户设置了缓存保存的时间,在请求返回成功后,就会写入缓存。
我们接着往下看,对于缓存,YTKNetwork保存的是两种缓存:
第一种是纯粹的NSData类型的实例。第二种是描述当前NSData实例的元数据YTKCacheMetadata的实例,从它的属性来看,分为这几种:
- 缓存的版本,默认返回为0,用户可以自定义。
- 敏感数据,类型为id,默认返回nil,用户可以自定义。
- NSString的编码格式,在YTKNetworkPrivate内的YTKNetworkUtils实现。
- 元数据的创建时间。
- app的版本号,在YTKNetworkPrivate内的YTKNetworkUtils实现。
在将元数据的实例的这些属性都被赋值以后,将元数据实例序列化写入磁盘中。保存的路径通过cacheMetadataFilePath
方法获取。
现在知道了YTKRequest的缓存内容,我们来看一下这两种缓存的位置:
1 | //YTKRequest.m |
可以看出,纯NSData数据缓存的文件名包含了请求方法(GET,POST..),baseURL,requestURL,请求参数拼接的字符串再进行md5加密而成。
而元数据的的文件名则在纯NSData数据缓存的文件名后面加上了.metadata后缀。
为了更形象地看到这两种缓存,我将缓存的保存时间设置为200秒之后再请求一次,然后打开文件夹找到了它们:
而且我们也确认了保存所有YTKNetwork缓存的文件夹的名字为LazyRequestCache。
OK,现在我们知道了在请求成功回调后的缓存写入,接下来看一下是如何回调的:
1 | //YTKNetworkAgent.m |
我们可以看到,代理的回调是先于block的回调的。而且在block回调结束以后,会立即调用clearCompletionBlock方法将block清空。该方法的实现是在YTKBaseRequest里:
1 | //YTKBaseRequest.m |
现在我们知道了请求成功的处理,那么再来看一下请求失败时的处理:
1 | //YTKNetworkAgent.m |
在这个方法里,首先判断了当前任务是否为下载任务,如果是,则储存当前已经下载好的data到resumableDownloadPath
里面。而如果下载任务失败,则将其对应的在本地保存的路径上的文件清空。
到这里,我已经把单个请求从配置,发送,响应,回调的步骤都讲解完了。为了帮助大家理解整个过程,这里提供了整个的流程图:
我们说YTKNetworkAgent是请求的发送者,既然有发送,也就会有取消等操作,这就不得不提它的另外两个接口:
1 | //YTKNetworkAgent.h |
首先我们看下取消某个request这个方法的实现:
1 | //YTKNetworkAgent.m |
取消所有在字典里添加的request:
1 | //YTKNetworkAgent.m |
这个stop方法是在YTKBaseRequest里面定义的:
1 | //YTKBaseRequest.m |
OK,看到这里,相信你对YTKNetwork单个请求的流程有了比较好的了解了,下面我们来看一下YTKNetwork的高级功能:批量请求和链式请求。
3.3 批量请求和链式请求
YTKNetwork支持的批量请求有两种:
- 批量请求:多个请求几乎同时发起。
- 链式请求:当前个请求结束后才能发起下一个请求。
其实无论是批量请求,还是链式请求,我们都可以想到很可能是用一个数组将这些请求管理了起来。那么具体是如何实现的呢?
我们首先来看一下YTKNetwork是如何实现批量请求的。
3.31批量请求
YTKNetwork 使用YTKBatchRequest类来发送无序的批量请求,它需要用一个含有YTKRequest子类的数组来初始化,并将这个数组保存起来赋给它的_requestArray
实例变量:
1 | //YTKBatchRequest.m |
初始化以后,我们就可以调用start方法来发起当前YTKBatchRequest实例所管理的所有请求了:
1 | //YTKBatchRequest.m |
在这里,我们可以看出:
- 在至少完成了其中一个请求以后,调用当前YTKBatchRequest实例的start方法会立即返回,否则可以无限制start。
- YTKBatchRequest的实例是需要在发起请求之前,要被添加在YTKBatchRequestAgent里的数组里:
1 | //YTKBatchRequestAgent.m |
- 因为是批量发送请求,所以在这里是遍历YTKBatchRequest实例的_requestArray并逐一发送请求。因为已经封装好了单个的请求,所以在这里直接start就好了。
发起请求以后,在每个请求回调的代理方法里,来判断这次批量请求是否成功。
YTKRequest子类成功的回调:
1 | //YTKBatchRequest.m |
我们可以看到,在某个请求的回调成功以后,会让成功计数+1。在+1以后,如果成功计数和当前批量请求数组里元素的个数相等,则判定当前批量请求成功,并进行当前批量请求的成功回调。
接下来我们看一下某个请求失败的处理:
YTKReques子类失败的回调:
1 | //YTKBatchRequest.m |
在这里不难看出,当前批量请求里面只要有一个request失败了,则判定当前批量请求失败。
而当前批量请求失败的回调(代理和block)会传入这个失败的request的实例。而且这个失败的request会先被赋给_failedRequest这个实例变量里。
总的来说,YTKBatchRequest类用一个数组来保存当前批量请求所要处理的所有request实例。而且用一个成功计数来判定当前批量请求整体是否成功。而当前批量请求的失败则是由这些request实例里面第一个失败的实例导致的:只要有一个request回调失败了,则立即停止其他的所有请求并调用当前批量请求的失败回调。
现在讲完了批量请求的处理,我们接下来看一下链式请求的处理。
3.32链式请求
和批量请求类似,处理链式请求的类是YTKChainRequest,并且用YTKChainRequestAgent单例来管理YTKChainRequest的实例。
但是和批量请求不同的是,YTKChainRequest实例的初始化是不需要传入一个含有request的数组的:
1 | //YTKChainRequest.m |
但是它提供了添加和删除request的接口:
1 | //YTKChainRequest.m |
注意,在给YTKChainRequest实例添加request实例的同时,还可以传入回调的block。当然也可以不传,但是为了保持request数组和callback数组的对称性(因为回调的时候是需要根据request数组里的index来获取callback数组里对应的callback的),YTKNetwork给我们提供了一个空的block。
我们接着看一下链式请求的发起:
1 | //YTKChainRequest.m |
我们可以看到,YTKChainRequest用_nextRequestIndex
来保存下一个请求的index,它的默认值是0。而它的值的累加是在当前请求结束后,发起下面的请求之前进行的。所以说,如果已经完成了请求队列里的第一个请求,就无法在启动当前的请求队列了,会立即返回。
这里startNextRequest
方法比较重要:在判断请求队列数组里面还有request的话,就会调用这个方法:
1 | //YTKChainRequest.m |
这个方法有两个作用:
- 第一个作用是判断是否能进行下一个request(如果index 大于或等于 request数组的count的话就不能在request数组里取出request,因为会造成数组越界)
- 第二个作用是如果可以进行下一个request,则发起该request。并将
_nextRequestIndex+1
。
所以和批量请求不同的是,链式请求的请求队列是可以变动的,用户可以无限制地添加请求。只要请求队列里面有请求存在,则YTKChainRequest就会继续发送它们。
现在我们知道了YTKChainRequest的发送,接下来看一下回调部分:
和YTKBatchRequest相同的是,YTKChainRequest也实现了YTKRequest的代理:
1 | //某个request请求成功的代理的实现 |
我们可以看到,在某个request回调成功以后,会根据当前请求的index(_nextRequestIndex-1)来获取其对应的block并调用。接着,再调用startNextRequest
方法来判断当前的YTKChainRequest的请求队列里面是否还有其他的请求了:
- 如果没有了,则调用当前YTKChainRequest的最终成功的回调。
- 如果还有,则发起接下来的request(按顺序)。
接下来我们再看一下某个request失败的代理的实现:
1 | //YTKChainRequest.m |
如果当前的request请求失败了,则判定当前链式请求是失败的,则立即调用当前链式请求的失败回调。
现在我们知道了链式请求的请求和回调,再来看一下链式请求的终止:
1 | //YTKChainRequest.m |
这个stop
方法是可以在外部调用的,所以用户可以随时终止当前链式请求的进行。它首先调用clearReuqest
方法,将当前request停止,再将请求队列数组和callback数组清空。
1 | //YTKChainRequest.m |
然后在YTKChainRequestAgent单例里面,将自己移除掉。
4. 最后的话
阅读这个框架的源码我的收获是:加深了对命令模式,对Block的理解,知道了一个网络请求都需要什么元素组成,知道了网络缓存该怎么设计,也知道了链式请求怎么设计等等。
我还记得当初听说YTKNetwork能发起链式请求的时候觉得毫无思路的感觉,不过现在应该没什么问题了。
所以说多阅读源码对技术水平的提升是很有帮助的,除了能增多对本语言API的了解,其实更有意义的是它能让你接触到一些新的设计和解决问题的办法,这些都是脱离某个语言本身的东西,也是作为一名程序员所必不可少的东西。
希望这篇文章能对读者们有所帮助~