仅需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}];

至此,一个完整的网络模块就完成了。

2 0
原创粉丝点击