仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装
来源:互联网 发布:lol选手符文天赋 知乎 编辑:程序博客网 时间:2024/04/29 23:44
程序框架确定了,还需要封装网络模块。
一个丰富多彩的APP少不了网络资源的支持,毕竟用户数据要存储,用户之间也要交互,用户行为要统计等等。
使用开源框架
俗话说得好,轮子多了路好走,我们不需要自己造轮子,拿来主义就行了。
android网络模块核心功能使用xUtils3开源框架来完成。
而iOS则使用AFNetWorking,别告诉我你没听说过AFNetworking。
xUtils3拥有4大功能:数据库,视图注解,网络,图片(支持webp)。
AFNetWorking则包含网络和图片2部分。
我们只需要用到其中的网络模块和图片缓存模块。
Model(Record)封装:
《App研发录》中强烈要求把后台返回的json数据转换成类实例Record(有些人喜欢称为model,即:MVC中的M,而个人习惯称之为Record,而Model的使用我更倾向于可共享可本地化的全局单例)类。在业务逻辑中使用的是这些类实例化后的对象。
这样做的好处有3个:
- 不易出错。JSONObject对象操作起来有点麻烦,比如:每次需要使用has方法来判断某些值是否存在,如果不判断,而这些值恰好不存在,则会崩溃。更重要的是,需要使用字符串来做键,写错了也是没有编译器提示的。
- 数据传递更为容易。页面间,对象间传递数据可直接传递Record对象,更加具有可读性,也更高效。如果传递JSONObject则需要再次解析。从而造成同一个数据多次解析。
- 代码更加规范。可以把Json转Record封装到网络层,从而在使用者看来,网络请求回来的数据就是Record。这样更容易让不同的程序员做更少的事情,从而写出尽量类似的代码。
为了达到上述目的,我们需要再次引入一个第三方库,来自Google的Gson,如何引入及如何调用请另行查询,它的作用就是把json字符串转换成本地类对象。
iOS则需要引入另一个第三方库,MJExtension。这个库作用同Android的Gson,但是相对android来说,它更加强大,更加易用。使用MJExtension的方法请见官方Demo。
然后我们需要建立一个基类BaseRecord来表示网络数据的基类,它是一个空的类,实现了Serializable这个接口,目的是让它可以通过Intent传递,也可以方便的本地化(把对象写入到硬盘)。
//android://BaseRecord.javapublic class BaseRecord implements Serializable{}
//iOS://BaseRecord.h@interface BaseRecord: NSObject@end
//BaseRecord.m@implements BaseRecord@end
后续所有的表示服务端返回的数据都需要继承BaseRecord这个类,这样写在设计模式中对应的说法是:里氏替换。
至于具体的record如何写,如何使用Gson进行绑定,下面代码中有部分内容,更多细节请自行查询资料。
这里提供一个json自动转java类的网址作为参考。
ServerBinder的封装:
为了达到上述目的,让使用者用最简单的方法就能够获取到网络资源,我们需要封装一个类,ServerBinder。
ServerBinder是一个单例,它需要用户输入后台接口的名字后,然后输出一个对应的存储了所有返回的服务端数据的Record。
ServerBinder中需要这样一个方法:regist,表示注册某个接口,只有在ServerBinder中注册过的服务端接口,留下了必要信息,后续才能够调用。
我们需要分析一下服务端调用地址的构成,来决定此方法的传入参数:
服务端接口往往是这样的,http://xxx.com/api/user_info?id=1000
其中可变的部分为:
- http://xxx.com:表示服务器地址
- api:服务端入口
- user_info:接口名,
- ?后面表示参数。
这样,我们的regist函数包括5个参数:网址,服务端入口,接口名,接口类型(get还是post),还有返回的record的类型。此函数需要做到,把地址,入口,方法名,record类型 存储起来。存储的数据需以方法名为键。此方法全局只需调用一次。
以方法名为键的原因是:对于服务端来说,同一个方法名对应的数据格式是相同的。
我们还需要一个方法:call,来表示调用此接口,可以在任何需要网络数据的时候调用它。
call方法需要3个参数,方法名,参数列表,还有回调函数(实现为一个内部接口,供调用者实现,类似观察者模式,但是这个观察者寿命比较短,只能观察一次)。
用户调用call方法时,所需要的数据都有了。返回的数据需要在真正的服务端回调中处理,把json转成record,然后把结果交给上面说的观察者即可。
另外每次服务端数据返回,都会带有当前服务器时间,因此客户端需要做时间校正:令app客户端每次获取的时间都是服务器时间,避免用户修改设置里面的手机时间,导致app内时间错误。
好了,知道了上面的内容,我们就可以写一份完整的封装网络数据的类了。内容如下(下面代码仅是伪代码,使用时请自行调试)。
//android://ServerBinder.javapublic class ServerBinder{ private final static String TAG = "ServerBinder"; private long timeOffset = 0;//服务器时间和本地时间的差值 //单例 private ServerBinder(){} private static ServerBinder sBinder = null; public sythornized ServerBinder getInstance(){ if(sBinder == null){ sBinder = new ServerBinder(); } return sBinder; } //保存所有注册的数据,当然要保存了,不保存怎么调用? private HashMap<String, BindData> mBindDatas; //表示注册的服务端数据 public static class BindData{ public String addr;//服务端地址 public String entry;//服务端代码入口 public String ifaceName;//接口名 public String ifaceType;//接口类型 public Class <?> recordClass;//返回record类型 } //服务端返回数据 public static class ServerData{ public BindData bindData;//注册数据,让你分辨是什么接口及参数 public BaseRecord serverRecord;//服务端返回的数据 public int status;//接口调用状态 status为1表示成功,为0表示失败 public String message;//服务端返回的错误或提示信息 } //客户端回调接口 public interface ServerCallback{ //status 表示网络请求状态,bindData表示当前请求相关参数,record表示返回数据 public void onServerCallback(ServerData data); } //注册!! public void regist(String addr, String entry, String ifaceName, String ifaceType, Class<?> recordClass){ //初始化BindData BindData data = new BindData(); data.addr = addr; data.entry = entry; data.ifaceName = ifaceName; data.recordClass = recordClass; data.ifaceType = ifaceType; //把数据存起来 mBindDatas.put(entry, data); } //客户端调用接口,注意接口参数,params是一个字符串数组,后端是无类型的php,可以这样写,但是如果后端是java则需要修改。或者可以用json。 public void call(String ifaceName, ServerCallback cb, String ...params){ if(!mBindDatas.contains(ifaceName)){ Log.e(); return; } BindData bindData = mBindDatas.get(ifaceName); switch(bindData.ifaceType){ case "get": get(bindData, params, cb); break; case "post": post(bindData,params, cb); break; case "download": download(bindData, params, cb); break; case "upload": upload(bindData,params, cb); break; } } /* 假设服务端数据格式为: { "status": 1,//1表示正确 0表示错误 "time":17383592394, "message": "一切正常", "data":{ //需要转换成record的部分 } } */ private void handleResponse(BindData bindData, String jsonStr, ServerCallback cb){ JSONObject jsonObj = new JSONObject(jsonStr); ServerData serverData = new ServerData(); serverData.bindData = bindData; serverData.status = jsonObj.getInt("status"); serverData.message = jsonObj.getString("message"); if(serverData.status == 1){ String data = jsonObj.getObject("data").toString(); serverData.serverRecord = (BaseRecord)new Gson().fromJson(data, bindData.recordClass); } cb.onServerCallback(serverData); //时间校正 if(jsonObj.contains("time")){ long time = jsonObj.getLong("time"); timeOffset = time - getLocalTime(); } } public long getLocalTime(){ return System.currentTimeMillis();//毫秒,注意时间单位的统一。 } public long getServerTime(){ return getLocalTime() + timeOffset; } // 下面就是真正调用接口了 // 另外iOS版本的ServerBinder,除了下面的4个函数内容不一样之外,其余部分逻辑完全一致。 // 只需要把java翻译成objective-c即可。 public void get(BindData bindData, String[]params, ServerCallback cb){ //...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 handleResponse(bindData, jsonStr, cb); } public void post(BindData bindData, String[]params, ServerCallback cb){ //...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 handleResponse(bindData, jsonStr, cb); } public void download(BindData bindData, ServerCallback cb){ //...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 handleResponse(bindData, jsonStr, cb); } public void upload(BindData bindData, String[]params, ServerCallback cb){ //...TODO 使用xutils接口获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 handleResponse(bindData, jsonStr, cb); }}
//ServerBinder.h#import <Foundation/Foundation.h>//表示注册的服务端数据@interface BindData : NSObject@property (nonatomic, copy) NSString *addr;@property (nonatomic, copy) NSString *entry;@property (nonatomic, copy) NSString *ifaceName;@property (nonatomic, copy) NSString *ifaceType;@property (nonatomic, copy) Class recordClass;@end//表示服务端返回数据@interface ServerData : NSObject@property (nonatomic, strong) BindData *bindData;@property (nonatomic, strong) BaseRecord *serverRecord;@property (nonatomic, unsafe_unretained) NSInteger status;@property (nonatomic, copy) NSString *message;@end//客户端回调接口typedef void(^ServerCallbacka)(ServerData *);@interface ServerBindera : NSObject//单例+(instancetype) getInstance;//注册接口-(void) registWithAddr:(NSString *)addr entry:(NSString *)entry ifaceName:(NSString *)ifaceName ifaceType:(NSString *)ifaceType clazz:(Class) clazz;//调用接口-(void) callWithIfaceName:(NSString *)ifaceName cb:(ServerCallback) cb params:(NSDictionary *)params;//获取当前服务器时间-(NSInteger) getServerTime;@end
//ServerBinder.m#import "ServerBinder.h"@implementation BindData@end@implementation ServerData@end@implementation ServerBinder{ NSInteger mTimeOffset;//服务器时间和本地时间的差值 NSMutableDictionary *mBindDatas;//保存所有注册的数据,当然要保存了,不保存怎么调用?}+(instancetype) getInstance{ static ServerBinder *binder = nil; static dispatch_once_t dispatchOnce; dispatch_once(&dispatchOnce, ^{ binder = [[ServerBinder alloc] init]; }); return binder;}//注册某接口,只有注册过的接口才能使用 call 方法调用。全局每个接口只需调用一次-(void) registWithAddr:(NSString *)addr entry:(NSString *)entry ifaceName:(NSString *)ifaceName ifaceType:(NSString *)ifaceType clazz:(Class) clazz{ BindData *data = [[BindData alloc] init]; data.addr = addr; data.entry = entry; data.ifaceName = ifaceName; data.recordClass = clazz; data.ifaceType = ifaceType; [mBindDatas setObject:data forKey:entry];}//调用某接口,在任何需要数据的时候调用。-(void) callWithIfaceName:(NSString *)ifaceName cb:(ServerCallback) cb params:(NSDictionary *)params{ if (![mBindDatas containsKey:ifaceName]) { NSLog(@"cant find this ifaceName: %@", ifaceName); return; } BindData *bindData = [mBindDatas objectForKey:ifaceName]; if ([bindData.ifaceType isEqualToString:@"get"]) { [self getWithBindData:bindData andParams:params cb:cb]; }else if ([bindData.ifaceType isEqualToString:@"post"]) { [self postWithBindData:bindData andParams:params cb:cb]; }else if ([bindData.ifaceType isEqualToString:@"download"]) { [self downloadWithBindData:bindData andParams:params cb:cb]; }else if ([bindData.ifaceType isEqualToString:@"upload"]) { [self uploadWithBindData:bindData andParams:params cb:cb]; }}//处理服务器返回数据-(void) handleResponseWithBindData:(BindData *) bindData jsonDict:(NSDictionary *)jsonDict cb:(ServerCallback)cb{ ServerData *serverData = [[ServerData alloc] init]; serverData.bindData = bindData; serverData.status = [[jsonDict objectForKey:@"status"] intValue]; serverData.message = [[jsonDict objectForKey:@"message"] stringValue]; if (serverData.status == 1) { id data = [jsonDict objectForKey:@"data"]; //把json数据转换成Record serverData.serverRecord = [[[bindData.recordClass alloc] init]mj_setKeyValues:[data mj_JSONObject]]; } if (cb) { cb(serverData); } //同步服务器时间 if ([jsonDict containsKey:@"time"]) { NSInteger time = [[jsonDict objectForKey:@"time"] longValue]; mTimeOffset = time - [self getLocalTime]; }}-(NSInteger) getLocalTime{ //TODO 返回本地当前时间 return 0;}-(NSInteger) getServerTime{ return [self getLocalTime] + mTimeOffset;}-(void) getWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{ //...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 [self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];}-(void) postWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{ //...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 [self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];}-(void) downloadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{ //...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 [self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];}-(void) uploadWithBindData:(BindData *)bindData andParams:(id)params cb:(ServerCallback)cb{ //...TODO 使用AFNetWorking获取网络数据,然后返回值交给handleResponse处理 //...此部分不在本文范围内,需自行完成 //服务端数据回调时调用,当前只是示例不是真正调用位置 [self handleResponseWithBindData:bindData jsonDict: jsonDict cb:cb];}@end
程序如何使用上述代码进行网络注册和调用呢?
android:
1. 需要自定义Application 假设定义为 MyApplication。
2. 在MyApplication中注册xUtils。
3. 新建某个接口对应的Record类: XXXRecord.java,这个类应该继承BaseRecord,具体写法参照。
4. 在MyApplication的onCreate方法中,添加代码:
ServerBinder.getInstance().regist("http://www.xxx.com", "api", "get_user_info", "get", XXXRecord.class);
5.在需要调用接口的地方这样写:
ServerBinder.getInstance().call("get_user_info", new ServerCallback(){ @Override public void onServerCallback(ServerData data){ //data中包含很多数据,其中 data.serverRecord 就是我们的XXXRecord的实例了。 XXXRecord *record = (XXXRecord)data.serverRecord; }}, "uid", "1");
iOS:
1. 新建某个接口对应的Record类:XXXRecord,请参照MJExtension及其demo进行创建。
2. 在AppDelegate的如下方法中:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
添加代码
[[ServerBinder getInstance] registWithAddr: @"http://www.xxx.com" entry:@"api" ifaceName:@"get_user_info" ifaceType:@"get" Class:[XXXRecord class]];
3 . 在需要调用的地方这样写:
[ServerBinder getInstance] callWithIfaceName:@"get_user_info" cb:^(ServerData *serverData){ //serverData中包含很多数据,其中 serverData.serverRecord 就是我们的XXXRecord的实例了。 XXXRecord *record = (XXXRecord *)serverData.serverRecord;} params:@{@"uid":1}];
至此,一个完整的网络模块就完成了。
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(1):确定框架方案
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(5):数据持久化
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(6):各种公共方法及工具类的封装
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(3):构造具有个人特色的MVP模式
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(2):MVP比MVC更好吗
- 揭开手机app中摇一摇的神秘面纱
- Netty学习系列(一)-- 掀开你神秘的面纱
- GIS:揭开你神秘的面纱
- GIS:揭开你神秘的面纱
- PhoneGap揭开你的神秘面纱
- PhoneGap揭开你的神秘面纱
- 带你一步一步揭开Rxjava2.0的神秘面纱(6)
- 带你一步一步揭开Rxjava2.0的神秘面纱(4)
- mushup的神秘面纱
- block 神秘的面纱
- EJB的神秘面纱
- HashMap的神秘面纱
- ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务
- 带表情符号的自定义键盘
- C++函数的默认参数
- poj 1979 Red and Black (简单BFS)
- 【BZOJ2326】【codevs2314】数学作业,第100篇博文纪念
- 仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装
- linux命令积累
- 机器学习实战学习1:python实现KNN
- https://www.linkedin.com/in/joshuazhu wilson chen
- Mysql引擎 Innodb和MyISAM的区别
- LinearLayout 点击事件 添加点击事件代码
- Android研究院之应用开发线程池的经典使用
- tableview取消CELL的多余行数
- 理解inode