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

至此,一個完整的網絡模塊就完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章