
网络请求的构建很简单, 根据一个请求需要的条件如URL, 请求方式, 请求参数, 请求头等定义请求生成的接口即可. 定义如下:

可以看到方法参数都是生成请求基本组成部分, 当然, 这里的参数比较少, 因为在我的项目中像请求超时时间都是一样的, 类似这些公用的设置我都偷懒直接写在请求配置文件里面了. 我们看看请求接口的具体实现, 以数据请求为例:


代码很简单, 接口根据参数调用urlStringWithPath:useHttps:通过BaseURL和URLPath拼装出完整的URL, 然后用这个URL和其他参数生成一个URLRequest, 然后调用setCommonRequestHeaderForRequest:设置公用请求, 最后返回这个URLRequest.
BaseURL来自HHService, HHService对外暴露各个环境(测试/开发/发布)下的baseURL和切换服务器的接口, 内部走工厂生成当前的服务器, 我的设置是默认连接第一个服务器且APP关闭后恢复此设置, APP运行中可根据需要调用switchService切换服务器. HHService定义如下:




请求的派发是通过一个单例HHNetworkClient来实现的, 如果把请求比作炮弹的话, 那么这个单例就是发射炮弹的炮台, 使用炮台的人只需要告诉炮台需要发射什么样的炮弹和炮弹的打击目标便可发射了. 另外, 应该提供取消打击的功能以处理不必要的打击的情况, 那么, 根据炮台的作用. HHNetworkClient定义如下:



代码很简单, 通过参数生成URLRequest, 然后通过AFHTTPSessionManager执行任务, 在任务执行前我们以task.taskIdentifier为key保持一下执行的任务, 然后在任务执行后我们移除这个任务, 当然, 外部也可以在必要的时候通过我们返回的task.taskIdentifier手动移除任务.
注意我们先声明一个NSMutableArray来标志taskIdentifier, 然后在任务生成后设置taskIdentifier[0]为task. taskIdentifier, 最后在任务完成的回调block中使用taskIdentifier[0]来移除这个已经完成的任务. 可能有人会有疑问为什么不直接使用task.taskIdentifier, block不是可以捕获task吗? 下面解释一下为什么这样写:
我们知道block之于函数最大的区别就在于它可以捕获自身作用域外的对象, 并在block执行的时候访问被捕获的对象, 具体的, 对于值类型对象block会生成一份此对象的拷贝, 对于引用类型对象block会生成一个此对象的引用并使该对象的引用计数+1(这里我们只描述非__block修饰的情况). 那么代入到上面的代码, 我们来一步一步分析:

我们把它拆开来看:

可以看到returnTask是我们实际存储的任务, 而task只是一个临时变量, 此时task指向nil, 那我们生成returnTask的block此时捕获到的task也就是nil, 所以在任务完成的时候我们的task.taskIdentifier一定是0, 这样写的结果就是dispathTable只会添加不会删除(系统的taskIdentifier是从0开始依次递增的), 当然, 因为进行中的returnTask我们是做了存储的, 所以在任务未完成的时候我们还是可以做取消的.

这样其实就是一个简单的引用变换题了, 我们来看看各个指针的指向情况:
suspend: pTask->NSObject block.pTask->nil pReturnTask->nil alloc: pTask-> NSObject block.pTask->NSObject pReturnTask->returnTask completed: pTask->returnTask block.pTask->NSObject pReturnTask->returnTask
可以看到在任务执行完成时我们访问block.pTask时也不过是我们一开始的占位对象, 所以这个方案也不行, 当然, 取消任务依然可用
事实上block.pTask确实是捕获了占位对象, 只是我们在那之后没有替换block.pTask指向到returnTask, 然而block.pTask我们是访问不了的, 所以这个方案行不通.

既然我们访问不了block.pTask那就访问block.pTask指向的对象嘛, 更改这个对象的内容不就相当于更改了block.pTask么, 大家照着2的思路走一下应该很容易就能想通, 我就不多说了.
2.多服务器的切换 关于多服务器其实我也没有实际的经验, 公司正在部署第二台服务器, 具体需求是如果访问第一台服务器总是超时或者出错, 那就切换到第二台服务器, 基于此需求我简单的实现一下:




假设认为APP在此次使用过程中网络任务的错误率达到10%那就应该切换一下服务器, 我们在任务派发前将任务总数+1, 然后在任务结束后判断任务是否成功, 失败的话将任务失败总数+1再判断是否到达最大错误率, 进而切换到另一台服务器.
另外还有一种情况是大部分服务器都挂了, 后台直接走APNS推送可用的服务器序号过来, 就不用挨个挨个切换了.
OK, 炮弹有了, 炮台也就绪了, 接下来看看如何使用这个炮台.



HHAPIManager对外提供数据请求和取消的接口, 内部调用HHNetworkClient进行实际的请求操作.
1.协议还是配置对象? HHAPIManager的接口我们并没有像之前一样提供多个参数, 而是将多个参数组合为一个配置对象, 下面说一下为什么这样做:

然后原来的老接口全都调用新接口shouldCache默认传NO, 不需要缓存的API不用做改动, 而需要缓存的API都得改调用新接口然后shouldCache传YES. 这样能暂时解决问题, 工作量也会小一些, 然后过了两天总监过来说, 为什么没有对API区分缓存时间? 还有, 我们又有新需求了. 呵呵!


其实最初的设计是走协议的, HHAPIManager遵守这个协议, 内部给上默认参数, dispatchTaskWithCompletionHandler:会去挨个获取这些参数, 各个子类自行实现自己自定义的部分, 这样以后就算有任何拓展, 只需要在协议里面加个方法基类给上默认值, 有需要的子类API重写一下就行了.

协议的方案其实很好, 也是我想要的设计. 但是协议是针对类而言的, 这意味着今后的每添加一个API就需要新建一个HHAPIManager的子类, 很容易就有了几百个API类文件, 维护起来很麻烦, 找起来很麻烦(以上是同事要求替换协议的理由, 我仍然支持协议, 但是他们人多). 所以将协议替换为配置对象, 然后API以模块功能划分, 每个模块一个类文件给出多个API接口 ,内部每个API搭上合适的配置对象, 这样一来只需要十几个类文件.
总之, 考虑到配置对象既可以实现单个API单个类的设计, 也可以满足同事的需求, 协议被换成了配置对象.
另外, 所有的block参数都不写在配置对象里, 而是直接在接口处声明, 看着别扭写着方便(block做参数和做属性哪个写起来简单大家都懂的).
2.简单的请求结果缓存器 上面简单提到了请求缓存, 其实我们是没有做缓存的, 因为我司HTTP的API现在基本上都被废弃了, 全是走TCP, 然而TCP的缓存又是另一个故事了.但是还是简单实现一下吧:






简单定义一个HHCache对象, 存放缓存数据, 缓存时间, 缓存时效, 然后HHNetworkCacheManager单例对象内部用NSCache存储缓存对象, 因为NSCache自带线程安全特效, 连锁都不用.
在任务发起之前我们检查一下是否有可用缓存, 有可用缓存直接返回, 没有就走网络, 网络任务成功后存一下请求数据即可.
3.请求结果的格式化 网络任务完成后带回的数据以什么样的形式返回给调用方, 分两种情况: 任务成功和任务失败.这里我们定义一下任务成功和失败, 成功表示网络请求成功且带回了可用数据, 失败表示未获取到可用数据. 举个例子: 获取一个话题列表, 用户希望看到的看到是一排排彩色头像, 如果你调用API拿不到这一堆数据那对于用户来说就是失败的. 那么没拿到数据可能是网络出错了, 或者网络没有问题只是用户没有关注过任何话题, 那么相应的展示网络错误提示或者推荐话题提示.
任务成功的话很简单, 直接做相应JSON解析正常返回就行, 如果某个XXXAPI有特殊需求那就新加一个XXXAPIConfig继承APIConfig基类, 在里面添加属性或者方法描述一下你有什么特殊需求, XXXAPI负责格式好返回就行了(所以还是一个API一个类好, 干净).
任务失败的话就麻烦一点, 我希望任何API都能友好的返回错误提示, 具体的, 如果有错误发生了, 那么返回给调用方的error.code一定是可读的枚举而不是301之类的需要比对文档的错误码(必须), error.domain通常就是错误提示语(可选), 这就要求程序员写每个API时都定义好错误枚举(所以还是一个API一个类好, 干净)和相应的错误提示.大概是这样子:


通用的错误枚举和提示语定义在一个.h中, 以后有新增通用描述都在这里添加, 便于管理. HHAPIManager基类会先格式好某些通用错误, 然后各个子类定义自己特有的错误枚举(不可和通用描述冲突)和错误描述, 像这样:


然后调用方一般情况下只需要这样:

当然, 情况复杂的话只能这样, 代码多一点, 但是有枚举读起来也不麻烦:

这里多扯两句, 请求的回调我是以(error, id)的形式返回的, 而不是像AFN那样分别给出successBlock和failBlock. 其实我本身是很支持AFN的做法的, 区分成功和错误强行让两种业务的代码出现在两个不同的部分, 这很好, 不同的业务处理就该在不同函数/方法里面. 但是实际开发中有很多成功和失败都会执行的操作, 典型的例子就是HUD, 两个block的话我需要在两个地方都加上[HUD hide], 这样的代码写的多了就会很烦, 而我又懒, 所以就成功失败都在一个回调返回了. 但是! 你也应该区分不同的业务写出两个不同方法(像上面那样做), 至于公用的部分就只写一次就够了.像这样:

再说一句, 即使你比我还懒, 不声明两个方法那也应该将较短的逻辑写在前面, 较长的写在后面, 易读, 像这样:

4.两个小玩意儿 文章到这基本上这个网络层该说的都说的差不多了, 各位可以根据自己的需求改动改动就能用了, 最后简单介绍下两个和它相关的小玩意儿就结尾吧:




看名字应该就知道这个是和dispatch_group_notif差不多的东西, 不过是派发的对象不是dispatch_block_t而是id<HHNetworkTask>. 代码很简单, 说说思路就行了.
强调一下, 绝对不应该直接调用HHNetworkClient或者HHAPIManger的dataTaskxxx...这些通用接口来生成task, 应该在该task所属的API暴露接口生成task, 简单说就是不要跨层访问. 每个API的参数甚至签名规则都是不一样的, API的调用方应该只提供生成task的相应参数而不应该也不需要知道这些参数具体的拼装逻辑.

日常请求中有很多接口涉及到分页, 然而毫无疑问分页的逻辑在每个页面都是一模一样的, 但是却需要每个调用页面都保持一下currentPage然后调用逻辑都写一次, 其实直接在API内部实现一下分页的逻辑, 然后对外暴露第一页和下一页的接口就不用声明currentPage和重复这些无聊的逻辑了. 像这样:



HHURLRequestGenerator: 网络请求的生成器, 公用的请求头, cookie都在此设置. HHNetworkClient: 网络请求的派发器, 这里会记录每一个服役中的请求, 并在必要的时候切换服务器. HHAPIManager: 网络请求派发器的调用者, 这里对请求的结果做相应的数据格式化后返回给API调用方, 提供请求模块的拓展性支持, 并提供合理的Task供TaskGroup派发.