谈谈 iOS 网络层设计

对于 CTNetworking 设计理念和笔者的理解,Casa Taloyum 给出了回复:

  1. 已发出的请求是不可能做到真正取消的,所以请求的取消在实现上就是“即使拿到数据也不回调给业务”。这个在CTNetworking里面是已经做好了的。
  2. Service的概念是用于封装第三方SDK的,例如我在Github上给到的Marvel API SDK和高德地图API SDK。一组API中某个API不规范的情况,是可以在Service的实现中或者APIManager的视线中给予适配的。这也就是为什么Service只是一个protocol而不是一个具体实现的原因。所以protocol方式定义的service不是缺点,它是功能。
  3. CTNetworking已经足够成熟了。我有一个目标是将所有的第三方API都以CTNetworking的方式封装,而完成这一目标的所有基础设施也都已经完善了,所以CTNetworking在朝一个生态的方向去发展。具体可以看我给到的那些示范工程:Marvel API SDK、高德地图API SDK。

CTNetworking的基础设施包括

  1. bash的代码自动生成脚本,可以提高工程师在离散型API架构下的工作效率。2. 基于CTMediator的配置管理,可以做到跨APP时的代码复用,例如Marvel key在不同app上可能会有不同的key
  2. 用于实现API DEMO ViewController的父类,它可以极大地便于将API工程以APP的形式给用QA去做测试,给开发去做调试或者当API文档用。
  3. CTJsbridge已经可以跟CTNetworking交互,H5工程师可以很方便地使用基于CTNetworking的网络API。

前言

基于 AFNetworking 的二次封装网上蛮多的,比较好一点的就是 CTNetworking 和 YTKNetwork,但是看了一下源码过后发现都有一些不足的地方,或者说不太能满足我们的业务需求。考虑到 AFNetworking 本身就为网络层做了很多事情,二次封装并非是个复杂的事情,所以索性自己写了个便于拓展和维护 (代码完全脱敏):

代码地址和用法 : YBNetwork

参考思路:iOS应用架构谈 网络层设计方案
参考源码:YTKNetwork CTNetworking

调研

Casa Taloyum 前辈的文章对笔者的架构思维有着深远的影响,记得两年多前入行不久,看得一知半解,近些时间要做架构方面的工作,又去重温了一下。

如何设计一个好的网络层架构,在 Casa Taloyum 的文章中已经说得比较全面了。猿题库的 YTKNetwork 相对比较成熟,两份代码核心思想都是将代码归为集约处理部分和离散处理部分,在实现方式上有些差别。

没有什么技术难点,直接看了一遍两份开源代码,优点很多,这里罗列一下不足的地方(当然只是个人理解,并且笔者可能更多结合业务来考虑的):

CTNetworking 不足:

  • 使用 IOP 方式建立模块,化继承为组合。独立<CTServiceProtocol><CTAPIManagerInterceptor>等协议作为集约管理部分,若个别接口需要修改这些公共配置,只能在集约管理模块来判断,显得有一点繁琐。
  • 记录了一个 request 实例的所有 task,在 dealloc 中自动取消掉还未降落的网络请求,但是实际上网络请求任务会持有 request,所以自动取消策略不成立了。

YTKNetwork 不足:

  • 基于多态的设计思路,提供了很多供重载的方法,从设计来看,框架是可以实例化YTKBaseRequest子类 直接使用的,那么直接使用时无法重载这些方法专门定制(个人看来有些地方使用属性更灵活);并且,当一个 reqeust 多次start发起请求就会调用多次这些重载方法,可能造成多余计算;
  • 缓存策略使用一个YTKBaseRequest的子类YTKRequest来做,虽然这样看起来比较优雅,父类和子类各司其职,单一职责,但是缓存策略难免会更改父类的逻辑,如此就很难不违背开闭原则。框架的缓存只有一个失效时间控制,笔者想要拓展时发现要改的东西太多。
  • 同一个 request 实例多次 start 调用网络请求时 (多个网络请求并发情况),并未作出实际的处理策略,仅保留最新的NSURLSessionTask,而对旧的未结束的所有NSURLSessionTask丧失了控制权。
  • 网络请求任务强持有所有 request 对象,在弱网环境下可能会有大量 request 对象无法释放,而界面降落点可能不存在了。

共同不足:

  • 数据回调都是绑定在 request 上的,既然都未处理一个 request 重复并发请求的情况,那么多个网络请求落地时,request 上的数据会突变,业务方的处理方式是不可控的,既有可能在回调业务执行过程中发现数据变化了。

实际上针对团队的业务,架构上会有取舍,所以笔者列这些不足也可以说是比较片面的。

实现

如何进行离散请求调用?

在一个网络请求起飞到降落过程中,有一系列独有的配置始终能代表这一个网络请求。

那么思路就出来了,只要把一个针对某个接口的配置对象传递过去,让网络任务的闭包持有这个对象,然后在网络回调处理中,一直传递这个配置对象,像踢皮球一样,最终处理好后回调到业务类中。

怎么避免这个配置对象疯狂传递?实际上就可以把网络回调处理逻辑,放在这个配置对象中,就像CTNetworkingCTAPIBaseManager配置对象,只要安全落地就能命中对应的配置对象;也可以用一个全局容器把这些配置对象装起来,不用一直通过闭包传递,就像YTKNetworkYTKBaseRequest配置对象。

所以笔者之前用了一个奇怪的思路:

Config config = Config.new;
[NetworkManager startWithConfig:config success:^{} failure:^{}];

实际上这和上面两个框架道理是一样的,笔者内部也会写逻辑去管理所有config,但是这么做不好对单独的网络请求进行管理,非要管理的话,又需要去持有这个config了。

实现代码类:

  • YBNetworkManager : 负责组织数据发起网络请求,并且管理所有的 NSURLSessionTask
  • YBNetworkCache : 负责缓存处理
  • YBNetworkResponse : 回调响应结果
  • YBBaseRequest : 负责离散数据配置、网络响应处理逻辑

集约/离散配置方式

为了更加灵活,并没有采用 IOP 方式来做配置管理,而是采用继承的方式来做,为了提高灵活性,定制几率大的配置使用属性实现,需要重载的方法使用分类提出来看起来保证清晰。

在开发中,需要针对不同的接口团队创建不同的YBBaseRequest子类集约配置,比如DefaultServerRequest : YBBaseRequest。在使用时,可以直接实例化DefaultServerRequest或者子类化DefaultServerRequest进行离散配置。

主要思路和 YTKNetwork 基本一样,当然像 CTNetworking 这样强制子类化来使用接口更好管理,但是有些时候显得有些繁琐。

笔者这种处理方式虽然需要子类化一些YBBaseRequest进行公共配置,但是也保证了每一个请求接口实例都可以任意的定制集约管理部分,防止接口抽风。

重定向

网络落地重定向重写此方法:

- (void)yb_redirection:(void (^)(YBRequestRedirection))redirection response:(YBNetworkResponse *)response {
    // 同步或异步的做一些事情
    redirection(YBRequestRedirectionSuccess);
}

使用redirection闭包来达到可异步重定向的能力,在这之间可以做一些具体网络接口无感知的逻辑。比如拦截到所有接口都可能返回的一个错误状态码-1,需要向服务器验证身份,这里就可以直接让当前请求停止redirection(YBRequestRedirectionStop),然后发起异步验证的网络请求,验证成功后再重定向回调给业务redirection(YBRequestRedirectionSuccess),或重新发起网络请求[self start]

缓存处理

缓存处理专门提取一个类来包装逻辑,而调用逻辑仍然放在YBBaseRequest,实际上代码量很少,也好修改。

出于业务考虑,缓存支持的功能有:

  • 内存/磁盘存储方式
  • 缓存命中后是否继续发起网络请求
  • 缓存的有效时长
  • 定制缓存的 key

对于缓存命中的回调,笔者设置了专门的回调出口:

//Block
- (void)startWithCache:(nullable YBRequestCacheBlock)cache
success:(nullable YBRequestSuccessBlock)success
failure:(nullable YBRequestFailureBlock)failure;

//Delegate
- (void)request:(__kindof YBBaseRequest *)request cacheWithResponse:(YBNetworkResponse *)response;

对于 Block 方式 来说,独立的缓存回调闭包更好管理。
对于两种回调来说,设计一个专门的缓存回调能降低业务工程师的出错率。
对于网络及时数据和缓存数据往往在业务处理上有细微的差别,分开回调能避免出于疏忽而去写判断if (isCache) {...} else {...}(特别是当写业务的工程师并不知道这个 API 缓存策略是怎样的)。

缓存有效性验证

内部会在业务处理完成网络响应数据后尝试进行缓存,避免将异常数据写入缓存(比如数据导致 Crash 时)。且提供一个shouldCacheBlock可根据请求响应成功数据判断是否需要缓存(比如仅当 code == 0 时数据有效允许缓存)。

重复网络请求处理

提供三种方式:

  1. 允许重复网络请求
  2. 取消最旧的网络请求
  3. 取消最新的网络请求

举几个例子,当接口数据并不会在短时间变化时,重复发起网络请求就会浪费网络资源,可以选择方案 2 或 3;比如在搜索业务中,用户往往频繁的调用搜索接口,而发起一次搜索时,之前的搜索请求一般是没有意义了,就可以选用方案 2。

网络请求释放处理

提供三种方式:

  1. 网络任务会持有YBBaseRequest实例,网络任务完成YBBaseRequest实例才会释放
  2. 网络请求将随着YBBaseRequest实例的释放而取消
  3. 网络请求和YBBaseRequest实例无关联

实现网络任务对 YBBaseRequest 弱持有 ,当YBNetworkManager发起请求时,让回调闭包捕获弱引用的weakSelf的就行了。

而要让YBBaseRequest释放时自动取消网络请求只需要简单调用(不过在“网络请求和 YBBaseRequest 实例无关联”模式时是不能取消的)。

举几个例子,若你的控制器出栈以后希望取消未落地的网络请求,那么就使用方案 2,注意管理好 YBBaseRequest 的生命周期就行了;若你的网络请求是不论如何都不希望它取消的,那么使用方案 3;若你希望网络请求任务始终持有 YBBaseRequest 实例避免它提前释放,那么使用方案 1。

回调处理

为了让重复网络请求时,每次回调的数据不相互影响,笔者思来想去还是额外定义了一个类,而不是直接让YBBaseRequest持有。

至于为什么要单独定义一个类,其一是单独定义一个类便于拓展回调内容,并且也降低了框架内部数据流通过程中的成本(传递一个对象总比传递一堆对象好处理吧);其二在于一个 API 请求实例可能发起多次网络请求,从而可能就有多次网络落地,响应数据由YBBaseRequest持有会出现响应数据被覆盖情况。

后语

大体思路就是如此,至于线程安全啥的细节就不多说了,主要是在加锁的时候注意避免同一线程重复获取锁导致死锁就行了。

一个看似简单的二次封装也能有这么多值得思考的地方,精益求精并不是一件容易的事。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容