仅需6步,教你轻易撕掉app开发框架的神秘面纱(4):网络模块的封装

程序框架确定了,还需要封装网络模块。

一个丰富多彩的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.java
public 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.java
public 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}];

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

你可能感兴趣的:(ios,框架,android,网络)