欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS网络层架构设计1

程序员文章站 2022-08-09 19:20:55
前些天帮公司做了网络层的重构,当时就想做好了就分享给大家,后来接着做了新版本的需求,现在才有时间整理一下。 之前的网络层使用的是直接拖拽导入项目的方式导入了af,然后还修改了大量的,时隔2年,af已...

前些天帮公司做了网络层的重构,当时就想做好了就分享给大家,后来接着做了新版本的需求,现在才有时间整理一下。

之前的网络层使用的是直接拖拽导入项目的方式导入了af,然后还修改了大量的,时隔2年,af已经更新换代很多次了,导致整个重构迁移非常的麻烦。不过看着前辈写的代码,肯定也是一个高人,许多思路和我的一样,但是实现方式又不同,给我很好的参考。

在做网络层架构的时候也参考了casa大神的架构思想,但是还是有所不同。

本文没有太多的理论,没有太多的专业术语,一来是方便大家,二来我的基础也没那么好,没有太多华丽的词汇,对于架构来说主要是思路,有思路在,具体的实现就没有问题了

本文主要介绍以下几点:

1.网络接口规范

2.多服务器多环境设置

3.网络层数据传递(请求和返回)

4.业务层对接方式

5.网络请求怎么自动取消

6.网络层错误处理

无demo无文章

https://github.com/summertimsadness/networkingdemo

网络接口规范

demo里面的请求示例是在网上找的,不符合我说的这套规范,仅作示例用。

规范很重要,有合理的规范就可以精简很多代码逻辑,特别是接口的兼容,是最底层最基础的设计,把接口规范放在前面来说。

在做这次重构时,我提出了一些规范点,可以给大家参考

1.两层三部分数据结构

接口返回数据第一次为字典,分为两层三部分:code、msg、data

"code":0,

"msg":"",

"data":{

"upload_log":true,

"has_update":false,

"admin_id":"529ecfd64"

}

code:错误码,可以记录下来快速定位接口错误原因,可以定义一套错误码,比如200正常,1重新登录…

msg:接口文案提示,包括错误提示,用来直接显示给用户,所以这一套错误提示就不能是什么一串英文错误了

data:需要返回的数据,可以是字典,可以是数组

接口帮我们定义了code和msg,是不是我们就不需要做错误处理了?当然不是,服务端的错误逻辑毕竟是简单的,具体到data里面的数据处理可能还有错误,所以错误的处理是必不可少的,下面会单独对错误处理做介绍

2.网络请求参数上传方式统一

这里一般都能做到,也有额外的,比如我们的一个服务器接口做的比较早,当时post接口使用的就不规范,普通的应用信息channelid、device_id使用的是拼接在字符串后面的方式,而真正的请求参数则需要转成json放在一个字段里面传递,就是接口get、post并存的方式,造成网络层需要做特殊处理

所以说标准的get、post请求方式是很有必要的

3.关于null类型

大家都知道null类型在ios里面是很特殊的,我的建议是放在客户端来做,原因有很多:

1)接口的规范定义并不是每个公司都是从一开始就能定义好的,老接口如果要把null字段去掉的改动非常大

2)客户端用过一个接口过滤也可以解决,一劳永逸,不用再担心因为某天接口的问题出现崩溃,而且通过一些model的第三方库也可以很好的解决这个问题。这里不得说下swift的类型检测真是太方便了,之前一个项目用swift写的,代码规范一点,根本不会出现因为参数类型问题引起崩溃

多服务器多环境设置

这部分基本上是照搬casa大神的设计,这里我延伸了一个多环境的设计,小的项目一般都是一个服务器,但是像淘宝之类的项目一个服务器显然是不可能的,多个服务器的设计还是非常普遍的。根据一个枚举变量通过serverfactory单例生成获取对应的服务器配置

1.服务器环境

标准的app是有4个环境的,开发、测试、预发、正式,特别是服务器的代码,不能说所有的代码更改都在正式环境下,应该从开发->测试->预发->正式做代码的更新,开发就是新需求和优化的时候的更改,测试就是提交给测试人员后的更改,这个时候更改是在一个新的分支上,完成后要和合并到测试分支上并合并到开发分支上,预发这时候的变动就比较小了,一般会在测试人员完成后发布给全公司的人来测试,有问题了才会更改,更改后同样合并到开发分支,正式则是线上发布版本的紧急bug修复,修改完后同样合并到开发分支上。所以开发分支是一直都是最新的。在此基础上可能会有其他的环境,比如hotfix环境,自定义的h5/后台本地调试的环境。

客户端同样存在这些环境,并且要提供切换的入口。

在我的demo中提供了两套设置,一套是第一次安装应用的初始化环境(宏定义),另外是手动切换环境的设置(枚举environmenttype)。这里有一个比较绕的逻辑,宏定义的正式环境设置高于手动切换环境设置,手动切换环境设置高于宏定义其他环境

//宏定义环境设置

#if !defined ya_build_for_develop

//手动环境切换设置

#ifdef ya_build_for_release

//优先宏定义正式环境

self.environmenttype=environmenttyperelease;

#else

//手动切换环境后会把设置保存

nsnumber *type=[[nsuserdefaultsstandarduserdefaults]objectforkey:@"environmenttype"];

if(type){

//优先读取手动切换设置

self.environmenttype=(environmenttype)[typeintegervalue];

}else{

#ifdef ya_build_for_develop

self.environmenttype=environmenttypedevelop;

#elif defined ya_build_for_test

self.environmenttype=environmenttypetest;

#elif defined ya_build_for_prerelease

self.environmenttype=environmenttypeprerelease;

#elif defined ya_build_for_hotfix

self.environmenttype=environmenttypehotfix;

#endif

}

#endif

所以当宏定义正式环境存在的时候是不能手动切换环境的,用于普通用户的发布版本,但是其他宏定义环境时是可以切换到正式环境的。

半个坑

另外手动切换自定义的环境是在基类中实现的,而其他的环境配置是在协议中实现的,这就和其他环境地址的配置不统一了。

可以这样理解,这里的基类是为了提供已返回值,协议是为了返回值的灵活,既然自定义环境的地址配置不需要灵活性,自然是放在基类好。思路是大方向,实现是灵活的,如果非要放在协议中实现也无不可以,无非是赋值粘贴几次一样的代码,但是一模一样的代码是我最不喜欢看到的,所以就放在基类了。如果有更好的解决方案欢迎提供

2.扩展性

model提供的是高扩展性,针对不同的不服务器添加更多的配置,比如方法,比如数据解析方法…前面提到了,统一的规范有的时候不是一时半会就能做好的,兼容就成了需求,这个时候不同服务器的个性化设置就可以在协议中声明并实现了,基类提供返回值就好

网络层数据传递(请求和返回)

 

iOS网络层架构设计1

 

client、baseengine/dataengine、requestdatamodel数据传递

网络请求的发生在我理解中分两步,一步是数据的整理,一步是生成request并发起请求,基于这个思想我拆分出了client和engine,然后又把urlrequestgenerator从client中拆分出来,engine拆分出了下层的baseengine和面向不同业务的dataengine,

而从baseengine到client,再到urlrequestgenerator是要做数据传递的,请求参数和返回参数,所以又有了requestdatamodel

requestdatamodel

@interfaceyaapibaserequestdatamodel:nsobject

/**

*网络请求参数

*/

@property(nonatomic,strong)nsstring *apimethodpath;//网络请求地址

@property(nonatomic,assign)yaservicetypeservicetype;//服务器标识

@property(nonatomic,strong)nsdictionary *parameters;//请求参数

@property(nonatomic,assign)yaapimanagerrequesttyperequesttype;//网络请求方式

@property(nonatomic,copy)completiondatablockresponseblock;//请求着陆回调

// upload

// upload file

@property(nonatomic,strong)nsstring *datafilepath;

@property(nonatomic,strong)nsstring *dataname;

@property(nonatomic,strong)nsstring *filename;

@property(nonatomic,strong)nsstring *mimetype;

// download

// download file

// progressblock

@property(nonatomic,copy)progressblockuploadprogressblock;

@property(nonatomic,copy)progressblockdownloadprogressblock;

@end

可以看出来requestdatamodel属性都是网络请求发起和返回的必要参数,这样做的好处真的是太大了,不知道大家有没有这样的场景:因为请求参数的不同做了好多方法接口暴露出去,最后调起的还是同一个方法,而且一旦方法写的多了,最后连应该调用哪个方法都不知道了。我就遇到过,所以现在我的网络请求调起是这样的:

//没有回调,没有其他的参数,只有一个datamodel,节省了你所有的方法

[[yaapiclientsharedinstance]callrequestwithrequestmodel:datamodel];

生成nsurlrequest是这样的:

nsurlrequest *request=[[yaapiurlrequestgeneratorsharedinstance]generatewithyaapirequestwithrequestdatamodel:requestmodel];

可以看到我的demo里面的yaapiclient类和yaapiurlrequestgenerator类方法至少,方法少就意味着逻辑简单明了,方便阅读,两个类的代码行数都是120行,120行实现了网络请求的发起和着陆,你能想象吗

另外requestdatamodel带来的另外一个好处就是高扩展性,你有没有遇到网络层需要添加删除一个参数导致调用方法修改了,然后很多地方都要修改方法?用requestdatamodel只需要添加删除参数就行了,只需要改方法体,这个改方法体和同时改方法名方法体是完全两个工作量。哈哈,有点卖虎皮膏药的感觉。这个的确是我的得意创新点。

client

client做两个操作,一个是生成nsurlrequest,一个是生成nsurlsessiondatatask并发起,另外还要暴露取消操作给engine,urlrequestgenerator是生成nsurlrequest,urlrequestgenerator会对datamodel进行加工解析,生成对应服务器的nsurlrequest

然后client通过nsurlrequest生成nsurlsessiondatatask。

client和urlrequestgenerator都是单例

-(void)callrequestwithrequestmodel:(yaapibaserequestdatamodel *)requestmodel{

nsurlrequest *request=[[yaapiurlrequestgeneratorsharedinstance]

generatewithrequestdatamodel:requestmodel];

afurlsessionmanager *sessionmanager=self.sessionmanager;

nsurlsessiondatatask *task=[sessionmanager

datataskwithrequest:request

uploadprogress:requestmodel.uploadprogressblock

downloadprogress:requestmodel.downloadprogressblock

completionhandler:^(nsurlresponse *_nonnullresponse,

id_nullableresponseobject,

nserror *_nullableerror)

{

//请求着陆

}];

[taskresume];

}

取消接口参考了casa大神的设计,使用nsnumber *requestid来做task的绑定,就不多做介绍了

baseengine/dataengine

engine或者说是apimanager在我的设计中既不是离散的也不是集约的

casa大神的理论

集约型api调用其实就是所有api的调用只有一个类,然后这个类接收api名字,api参数,以及回调着陆点(可以是target-action,或者block,或者delegate等各种模式的着陆点)作为参数。然后执行类似startrequest这样的方法,它就会去根据这些参数起飞去调用api了,然后获得api数据之后再根据指定的着陆点去着陆。比如这样:

[apirequeststartrequestwithapiname:@"itemlist.v1"params:paramssuccess:@selector(success:)fail:@selector(fail:)target:self];

离散型api调用是这样的,一个api对应于一个apimanager,然后这个apimanager只需要提供参数就能起飞,api名字、着陆方式都已经集成入apimanager中。比如这样:

@property(nonatomic,strong)itemlistapimanager *itemlistapimanager;

// getter

-(itemlistapimanager *)itemlistapimanager

{

if(_itemlistapimanager==nil){

_itemlistapimanager=[[itemlistapimanageralloc]init];

_itemlistapimanager.delegate=self;

}

return_itemlistapimanager;

}

// 使用的时候就这么写:

[self.itemlistapimanagerloaddatawithparams:params];

各自的优点就不说了,但是由此延伸出几个问题:

1.参数的传递使用字典对于网络层来说是不可知的,而且业务层需要去关注接口字段的变化,其实是没有必要的

2.离散型api会造成manager大爆炸

3.集约型会造成取消操作不方便

4.取消操作并不是每个接口必须的,如果写成部分离散的部分集约的,代码的整体结构…我是个有强迫症的人,看不得这样的代码

所以我的设计主要就解决了上面的这些问题

1.面向业务层的dataengine只传递必要的参数进来,不使用字典,比如

@interfacesearchdataengine:nsobject

+(yabasedataengine *)control:(nsobject *)control

searchkey:(nsstring *)searchkey

complete:(completiondatablock)responseblock;

@end

control暂时先不管,是做自动取消的,后面再介绍。

searchkey就是搜索的关键字

在调用的时候就是这样

self.searchdataengine=[searchdataenginecontrol:selfsearchkey:@"关键字"complete:^(iddata,nserror *error){

if(error){

nslog(@"%@",error.localizeddescription);

}else{

nslog(@"%@",data);

}

}];

2.我按业务层来划分dataengine,比如bbsdataengine、shopdataengine、userinfordataengine…每个dataengine里面包含各自业务的所有网络请求接口,这样就不会出现dataengine大爆炸,像我们的项目有300多个接口,拆分后有十几个dataengine,如果使用离散型api设计,那画面太美我不敢看