程序框架確定了,還需要封裝網絡模塊。
一個豐富多彩的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}];
至此,一個完整的網絡模塊就完成了。