谈谈 iOS 网络层设计

indulge_in 2019-04-10 13:58:23 3108

前言

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

代码地址和用法 : YBNetwork

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

参考源码:YTKNetwork CTNetworking

调研

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

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

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

CTNetworking 不足:

  • 使用 IOP 方式建立模块,化继承为组合。独立CTServiceProtocol协议类作为一个接口团队的公有配置,若针对一个接口团队的某一个特定接口需要特别处理,也就是需要专门定制CTServiceProtocol的某些协议方法,那就很棘手了(当然若接口方非常规范就没有这个顾虑)。

  • 记录了一个 request 实例的所有 task,在 dealloc 中自动取消掉还未降落的网络请求,但是实际上网络请求任务会持有 request,所以自动取消策略不成立了(估计是作者未完善的,因为博客中有写)。

YTKNetwork 不足:

  • 基于多态的设计思路,提供了很多供重载的方法,从设计来看,框架是可以实例化YTKBaseRequest子类 直接使用的,那么直接使用时无法重载这些方法专门定制(个人看来有些地方使用属性更灵活);并且,当一个 reqeust 多次start发起请求就会调用多次这些重载方法,可能造成多余计算;

  • 缓存策略使用一个YTKBaseRequest的子类YTKRequest来做,虽然这样看起来比较优雅,父类和子类各司其职,单一职责,但是缓存策略难免会更改父类的逻辑,如此就很难不违背开闭原则。框架的缓存只有一个失效时间控制,笔者想要拓展时发现要改的东西太多。

  • 同一个 request 实例多次 start 调用网络请求时 (多个网络请求并发情况),并未作出实际的处理策略,仅保留最新的NSURLSessionTask,而对旧的未结束的所有NSURLSessionTask丧失了控制权。

  • 网络请求任务强持有所有 request 对象,在弱网环境下可能会有大量 request 对象无法释放,而界面降落点可能不存在了。

共同不足:

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

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

实现

如何进行离散请求调用?

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

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

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

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

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进行公共配置,但是也保证了每一个请求接口实例都可以任意的定制集约管理部分,防止接口抽风。

缓存处理

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

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

  • 内存/磁盘存储方式

  • 缓存命中后是否继续发起网络请求

  • 缓存的有效时长

  • 定制缓存的 key

  • 根据请求响应成功数据判断是否需要缓存(比如仅当 code=0 时数据有效允许缓存)

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

/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 缓存策略是怎样的)。

重复网络请求处理

提供三种方式:

  1. 允许重复网络请求

  2. 取消最旧的网络请求

  3. 取消最新的网络请求

实现比较简单,具体采用哪种策略还需要根据业务谨慎选择:

- (void)start {
    if (self.isExecuting) {
        switch (self.repeatStrategy) {
            case YBNetworkRepeatStrategyCancelNewest: return;
            case YBNetworkRepeatStrategyCancelOldest: {
                [self cancel];
            }
                break;
            default: break;
        }
    }
    ...
}

网络请求释放处理

提供三种方式:

网络任务会持有YBBaseRequest实例,网络任务完成YBBaseRequest实例才会释放

网络请求将随着YBBaseRequest实例的释放而取消

网络请求和YBBaseRequest实例无关联

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

__weak typeof(self) weakSelf = self;
taskID = [[YBNetworkManager sharedManager] startNetworkingWithRequest:weakSelf uploadProgress:^(NSProgress * _Nonnull progress) {
    __strong typeof(weakSelf) self = weakSelf;
    if (!self) return;
    [self requestUploadProgress:progress];
} downloadProgress:^(NSProgress * _Nonnull progress) {
    __strong typeof(weakSelf) self = weakSelf;
    if (!self) return;
    [self requestDownloadProgress:progress];
} completion:^(YBNetworkResponse * _Nonnull response) {
    __strong typeof(weakSelf) self = weakSelf;
    if (!self) return;
    YBN_IDECORD_LOCK([self.taskIDRecord removeObject:taskID];);
    [self requestCompletionWithResponse:response cacheKey:cacheKey fromCache:NO];
}];

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

- (void)dealloc {
    ...
    if (self.releaseStrategy == YBNetworkReleaseStrategyWhenRequestDealloc) {
    [self cancel];
    }
}

回调处理

为了让重复网络请求时,每次回调的数据不相互影响,笔者思来想去还是额外定义了一个类,而不是直接让YBBaseRequest持有,并且同CTNetworking一样预定义了一个YBResponseErrorType,包含三种类型:超时、取消、无网。

至于为什么要单独定义一个类,而不是直接回调一个id respondsObject,因为有些业务中还需要其它数据,比如头部信息,那么单独定义一个类便于拓展回调内容,并且也降低了框架内部数据流通过程中的成本(传递一个对象总比传递一堆对象好处理吧)。

后语

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

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

作者:indulge_in

链接:/www0686jianshu0686com/p/fe0dd50d0af1

饿了么CEO:本季度将花费30亿人民币与美团竞争 半场狂刷16分+硬刚詹皇!你们的皇阿玛又回来了 托马斯超好友斯皮思 墨西哥世锦赛62杆破球场纪录 女子看电影时把脚搭在前排抖个不停 闹到最后被拘 进球gif-任桂辛铲射中柱 李影补射先拔头筹 高通拟在加州办事处裁员1500人 花旗遭证监破天荒重罚5700万 保荐瑞金有缺失 双色球开7注753万落4地 山东或1人独揽2259万 爱踢球的女生最美!听小玫瑰讲述自己的足球情缘 俄反对党领袖欲推美式两党制 被批“轮不到你们” 黑马、惜败、加洞 超级荔枝双山比洞赛演多场激战 梦回总决!关键5分+死亡抢断 社会扎哥降临休城
外媒头条:特朗普将于本周签署关税协议 歌词男性化?德女官员呼吁国歌改词遭默克尔拒绝 世界最后一头雄性北方白犀牛患重病 临近灭绝 著名物理学家史蒂芬-霍金去世 享年76岁 叙基地遭袭:以色列嫌疑最大 叙军反导成功率超六成 商务部回应对美进口高粱反倾销调查初裁:符合规则 “男版郎平”开启执教路 中国女子体操队渐复苏 95后微博认证“川航玻璃破裂事件乘客” 网友笑翻 王文涛当选黑龙江省省长(图/简历) 我国防部:10国军队将来华参加军事比赛 有四大亮点 进球gif-两连击!妖锋转身打门破僵 郑凯木爆射 高校学生实习做“猪倌”:扎针不易 人工配种更难
海南返程机票价格破万:涨价违法还是市场规律? 俄打造激光武器挑战美太空优势:可打击侦察卫星 妙心召回儿童床护栏:存结构安全隐患 涉及超十万件 山西人大常委会副主任高卫东已当选省总工会主席 普京说“北溪-2”项目将维持从乌克兰过境输气 四川纪委监委:“严书记舆情”相关情况已介入调查 武汉生物百白破疫苗不合格系分装设备短时故障造成 朝韩美能在没有中国参与下达成终战宣言?中方回应 拼完心气拼勇气 拼多多对“便宜无好货”说不 服装批发5元到15元 90后如何白手起家 冬天摆地摊卖什么最好 文科生可以报哪些专业 AG亚游集团