現在程序員的能力是越來越強了,新技術新想法不斷的涌現,然後並應用於實戰上,有時候自己會不經意的發現手頭上用的技術好老,有點脫節的感覺。爲了擺脫這種不爽的感覺(其實就是怕被同行歧視),咋辦?唯一的出路就是不斷的學習!學習!!學習!!!
一、概述
現在的手機APP,能在應用商店上看到的APP,差不多有95%以上都需要網絡需求。進而得出網絡組件在APP中的重要性是非常高的!既然如此重要,我們戰鬥強悍的程序員們爲了本着服務大衆的奉獻精神,創造出了很多非常優秀的開源網絡框架,例如android-async-http、volley、retrofit、OkHttp3等等。這其中現在最受開發者們歡迎且用的非常多的框架就是retrofit和OkHttp3了。今天我們先聊OkHttp3,等以後我有時間再來和大家聊聊retroft的封裝使用。
爲什麼要聊OkHttp3?我的答案就4個字“簡單好用”,我們開發者使用的框架的目的就是方便我們開發,工具越簡單、用起來舒服,你說開發者能不愛嗎?說了這麼多廢話,接下來就要開始進入開發正題了,我會已兩種實現模式向大家介紹OkHttp3的使用,一種是“基本模式”(也可以叫Demo學習模式),另一種是”高級模式“(也可以叫開發者模式),至於兩種模式的區別,請仔細看我接下來的介紹吧。
二、基本使用模式
基本使用模式(Demo學習模式)其實就是我對OkHttp3的一個最基本的使用介紹,沒有對其進行封裝優化,用起來很簡單,但侷限性很強,僅作爲Demo學習用。
使用之前首先我們要配置環境,這一點相當簡單,一句話就可以搞定。在Android
Studio環境,找到自己項目下的build.gradle文件,找到dependencies{},然後在裏面添加
compile
'com.squareup.okhttp3:okhttp:3.3.0' //okttp依賴
就可以用了。請大家注意,裏面的3.3.0是目前我寫這篇的文章的版本,大家以後在用的時候最好還是選擇最新的版本,因爲不同的版本里面的方法可能不同,我之前就遇到這個坑,2.0版有一些方法在3.0上就沒了,或者說被其他方法整合優化掉了,這一點請各位一定要注意更新。
搭建好環境後,咱們開始看看OkHttp3的核心類,一共有四個核心類。因此使用起來也就是分別使用這四個
1、OkHttpClient類:這個類用於處理請求,因此第一步就是要創建OkHttpClient實例
2、Request類:這個類用來構建請求參數,如url、請求方式、請求參數、header等
3、Call類:生成一個請求實例,這裏可以選擇設置是同步執行還是異步執行
4、Response:請求結果響應,包含HTTP響應,返回數據等,這裏面就可以拿到接口傳來時數據,然後就可以對數據進行處理了。
文字看的不清晰,咱們對着上面的四句話看看代碼具體是啥樣的。
// 1、拿到okhttpClient對象
OkHttpClient okHttpClient = new OkHttpClient();
// 2、構造Request
Request.Builder builder = new Request.Builder();
Request request = builder
.get()
.url(mBaseUrl + "login?username=zbq&userpassword=123")
.build();
// 3、將Request請求封裝爲Call
Call call = okHttpClient.newCall(request);
// 執行Call
// Response response = call.execute(); 同步執行
call.enqueue(new Callback() { // 異步執行
@Override
public void onFailure(Call call, IOException e) {
LogUtils.e("onFailure" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String res = response.body().toString();
LogUtils.e(response.body().toString());
// 4、response回調在子線程中而不是UI線程
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(res);
}
});
}
});
雖然代碼里加了註釋相信很多人也應該能看的懂,以異步的方式去執行請求,所以我們調用的是call.enqueue,將call加入調度隊列,然後等待任務執行完成,我們在Callback中即可得到結果。上面我用的Get請求,接下來看看Post請求是什麼樣子的吧 // 1、拿到okhttpClient對象
OkHttpClient okHttpClient = new OkHttpClient();
FormBody requestBody = new FormBody.Builder()
.add("username","huanhuan").add("userpassword","12345").build();
// 2、構造Request
// 2.1 Post請求需要構造FormBody
Request.Builder builder = new Request.Builder();
Request request = builder.url(mBaseUrl +"login").post(requestBody).build();
// 3、將Request請求封裝爲Call
Call call = okHttpClient.newCall(request);
// 執行Call
// Response response = call.execute(); 同步執行
call.enqueue(new Callback() { // 異步執行
@Override
public void onFailure(Call call, IOException e) {
LogUtils.e("onFailure" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String res = response.body().toString();
LogUtils.e(response.body().toString());
// response回調在子線程中而不是UI線程
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(res);
}
});
}
});
是不是Post請求的代碼要比Get要多一些,沒錯,大家都清楚,post的時候,參數是包含在請求體中的;所以我們通過FormBody添加多個String鍵值對,然後去構造RequestBody,最後完成我們Request的構造。上述就是OkHttp3的最基本用法,對比兩部分代碼,大家就能發現是不是有很多重複的地方。一般項目裏有很多調用網絡的地方,如果每一個功能模塊的網絡請求都這麼寫,那還不被項目老大給罵死,即羅嗦,而且可複用性低,所以上述代碼只適合小的演示Demo。接下來咱們開始寫點兒實際工作中用的例子。
先提前聲明一下,這部分內容有點多,而且每個人的想法可能都不一樣,我是學習了任志強老師以及張鴻洋的資料後做的總結吧,如果我寫的不對或者不好,還請各位大神指正。謝謝。
對着這張圖咱們來拆解分析一下,看看要分裝那些類
三、高級模式
這裏的高級模式其實就是開發者模式,和上面基本模式的不同之處就是“封裝”和“複用”。將OkHttp3在封裝一次,方便以後使用。
基本的用法上面我都提到了,接下來我們看看我爲封裝的OkHttp3網絡框架畫的思路圖:
Request封裝
首先看最左邊的request,這裏麪包含有請求參數、url、創建對象。請求參數一般都是以鍵值對的形式,比如uername
: haha,我們可以用HashMap<String, String>或HashMap<String, Object>進行存儲,然後將其封裝到RequestParams類,這樣我們在request請求時直接創建一個對象傳入參數即可。例如
public ConcurrentHashMap<String, String> urlParams = new ConcurrentHashMap<>();
public ConcurrentHashMap<String, Object> fileParams = new ConcurrentHashMap<>()
public void put(String key, String value) {
if (key != null && value != null) {
urlParams.put(key, value);
}
}
public void put(String key, Object object) throws FileNotFoundException {
if (key != null) {
fileParams.put(key, object);
}
}
請求參數封裝好後,接下來開始封裝Request核心邏輯。網絡請求通常情況下我們都是用的Get和Post請求,所以我們要寫兩個方法去分別做兩個操作,這兩個方法可以封裝到CommonRequest類中。這個類的作用就是接收請求參數,然後生成Request對象。例如Get請求
/**
* 創建一個Get請求
* @param url
* @param params
* @return 返回一個創建好的Get請求對象
*/
public static Request createGetRequest(String url,RequestParams params){
StringBuilder stringBuilder = new StringBuilder(url).append("?");
if(params !=null){
for (Map.Entry<String,String> entry : params.urlParams.entrySet()){
stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
return new Request.Builder().url(stringBuilder.substring(0,stringBuilder.length()-1))
.get().build();
}
這裏我將方法聲明爲static,就是把它當工具以後方便調用,這裏大家也可以用單例對象實現。參數裏面的RequestParams就是我們剛剛封裝的請求參數,中間那部分邏輯其實就是Get請求中拼接請求參數了。最後我們要構建一個Request對象將參數填充進去然後返回給我們的調用者OkHttpClient。接下來看看Post請求: /**
* 創建一個Post請求
* @param url
* @param params
* @return 返回一個創建好的Post請求對象
*/
public static Request createPostRequest(String url,RequestParams params){
FormBody.Builder mFormBodyBuild = new FormBody.Builder();
if(params !=null){
for (Map.Entry<String,String> entry : params.urlParams.entrySet()){
// 將請求參數遍歷添加到請求構建類中
mFormBodyBuild.add(entry.getKey(),entry.getValue());
}
}
// 通過請求構建類的build方法獲取到真正的請求體對象
FormBody formBody = mFormBodyBuild.build();
return new Request.Builder().url(url).post(formBody).build();
}
這裏大家請注意以下之前有些人可能看到網上用是FormEncodingBuilder,這裏爲什麼用FormBody?因爲OkHttp3.x,FormEncodingBuilder已被FormBody取代,所以請大家在用的時候一定要引用最新的網絡框架到工程中!!!FormBody的作用就是方便添加鍵值對錶單數據,這裏用到了構建者模式,他繼承了RequestBody,給大家看看裏面的部分源碼就明白原理是什麼
了public static final class Builder { private final List<String> names = new ArrayList<>(); private final List<String> values = new ArrayList<>(); public Builder add(String name, String value) { names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true)); values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true)); return this; } public Builder addEncoded(String name, String value) { names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, true, false, true, true)); values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, true, false, true, true)); return this; } public FormBody build() { return new FormBody(names, values); } }
這樣最左邊的Request請求部分咱們就封裝完畢,接下來開始封裝OkHttp的核心部分OkHttpClient。
OkHttpClient
首先咱們先來描述一下這個類的作用:請求的發送、請求參數的配置以及現在必要的https的支持。創建一個OkHttpClientCnter類。創建好後我們要做一些最基本的網絡配置。
首先咱們先來描述一下這個類的作用:請求的發送、請求參數的配置以及現在必要的https的支持。創建一個OkHttpClientCnter類。創建好後我們要做一些最基本的網絡配置。
// 爲我們的client去配置參數
static {
// 創建我們client對象的構建者
OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
// 是爲構建者填充超時時間
okHttpBuilder.connectTimeout(TIME_OUT, TimeUnit.SECONDS);
okHttpBuilder.readTimeout(TIME_OUT, TimeUnit.SECONDS);
okHttpBuilder.writeTimeout(TIME_OUT, TimeUnit.SECONDS);
// 運行請求重定向
okHttpBuilder.followRedirects(true);
// https支持
okHttpBuilder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
okHttpBuilder.sslSocketFactory(HttpsUtils.initSSLSocketFactory(), HttpsUtils.initTrustManager());
// 生成Client對象
mOkHttpClient = okHttpBuilder.build();
}
這裏我要提一下okHttpBuilder.sslSocketFactory(),這個方法設置用於保護HTTPS連接的套接字工廠和信任管理器,裏面可以設置對Https的一個支持,說實話這一塊涉及到網絡的SSL這一塊原理我也不是很清楚(好尷尬)也是參考別人的方法實現,我這裏把代碼貼出來大家直接用就好了。 public static SSLSocketFactory initSSLSocketFactory() {
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("SSL");
X509TrustManager[] xTrustArray = new X509TrustManager[]
{initTrustManager()};
sslContext.init(null,
xTrustArray, new SecureRandom());
} catch (Exception e) {
e.printStackTrace();
}
return sslContext.getSocketFactory();
}
public static X509TrustManager initTrustManager() {
X509TrustManager mTrustManager = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
};
return mTrustManager;
}
好了配置好基本的網絡參數後,就可以發送請求了,這裏是調用Call的異步處理。 /**
* 通過構造好的Get Request,Callback去發送請求
* @param request
* @param commCallback
* @return Call
*/
public static Call get(Request request, Callback commCallback){
Call call = mOkHttpClient.newCall(request);
call.enqueue(new commCallback);
return call;
}
這樣寫看起來好像OK了,不過仔細一看感覺還是少了些什麼,是不是沒有對Callback回調做一個統一的處理啊。的確,接下來我就對最右邊的部分Callback進行封裝,然後回來優化這塊邏輯。Callback封裝
這一塊的封裝要做的事情有點多,要處理回調函數,處理數據異常,轉發消息到UI線程最後還要將消息轉化成對應的實體,看起來好麻煩,的確。但是我想說如果這一層封裝的很好的話,以後調用時能夠省略好多好多代碼。何樂而不爲呢?接下來我們一個一個的拆解實現。
1、處理回調函數
首先我們需要自定義回調事件監聽,或許有人會問,Callback裏面不是已經有回調事件,爲什麼還要自定義?你看源碼不是有嗎
public interface Callback { /** * Called when the request could not be executed due to cancellation, a connectivity problem or * timeout. Because networks can fail during an exchange, it is possible that the remote server * accepted the request before the failure. */ void onFailure(Call call, IOException e); /** * Called when the HTTP response was successfully returned by the remote server. The callback may * proceed to read the response body with {@link Response#body}. The response is still live until * its response body is {@linkplain ResponseBody closed}. The recipient of the callback may * consume the response body on another thread. * * <p>Note that transport-layer success (receiving a HTTP response code, headers and body) does * not necessarily indicate application-layer success: {@code response} may still indicate an * unhappy HTTP response code like 404 or 500. */ void onResponse(Call call, Response response) throws IOException; }的確,源碼裏是有。但是!這不代表以後還是這些方法,我剛纔也提到了,由於OkHttp3的項目負責人太敬業了,總想讓自己的代碼更加完美好用,於是版本一升級,難免有的方法就過時消失或者被整合到其他方法中了,這個坑我在剛剛在講Request裏的FormBody就提到過了。所以,保險起見就是我們自己重新自定義一個回調事件接口,同時自定還有一個好處就是方便以後好擴展,大家看源碼裏面就只提供了 兩個方法,如果以後我想要看看當前下載進度如何,源碼裏沒有這個監聽下載進度的回調方法,那豈不是呵呵了。爲此我們要定義實現爲好。OK說完了該實現了
/**
* 自定義事件監聽
* Created by Administrator on 2017/7/23.
*/
public interface DisposeDataListener {
/**
* 請求成功回調事件處理
*/
public void onSuccess(Object responseObj);
/**
* 請求失敗回調事件處理
*/
public void onFailure(Object reasonObj);
}
寫完回調接口後,我們可以對回調做一下預處理操作,因爲我們在處理Json回調時,需要實現將Json對象轉化成實體對象,所以我們就需要一個轉化成對象的字節碼對象,因此我們可以將響應回調Listener和要轉換的字節碼對象進行封裝。例如/**
* 相當於對callBack的預處理
* Created by Administrator on 2017/7/23.
*/
public class DisposeDataHandle {
public DisposeDataListener mListener = null;
public Class<?> mClass = null; // 字節碼
public String mSource = null;
public DisposeDataHandle(DisposeDataListener listener)
{
this.mListener = listener;
}
public DisposeDataHandle(DisposeDataListener listener, Class<?> clazz)
{
this.mListener = listener;
this.mClass = clazz;
}
public DisposeDataHandle(DisposeDataListener listener, String source)
{
this.mListener = listener;
this.mSource = source;
}
}
2、異常處理
異常處理相比上面的處理回調函數就簡單多了,我只需要獲取異常的狀態碼和狀態信息就好了。實現如下:
/**
* 自定義異常
* Created by Administrator on 2017/7/23.
*/
public class OkHttpException extends Exception {
private static final long serialVersionUID = 1L;
/**
* the server return code
*/
private int ecode;
/**
* the server return error message
*/
private Object emsg;
public OkHttpException(int ecode, Object emsg) {
this.ecode = ecode;
this.emsg = emsg;
}
public int getEcode() {
return ecode;
}
public Object getEmsg() {
return emsg;
}
}
好了,接下來就是重頭戲轉發消息到UI線程以及將Json轉化成對應的實體。我將這兩個一起封裝到JsonCallbackCenter類中,一起實現。3、4:轉發消息到UI線程&將Json轉化成對應的實體
首先我們來創建一個JsonCallbackCenter類,然後再來配置一些和服務交互的參數以及基本消息傳遞的對象,例如:
首先我們來創建一個JsonCallbackCenter類,然後再來配置一些和服務交互的參數以及基本消息傳遞的對象,例如:
// 與服務器返回的字段的一個對應關係
protected final String RESULT_CODE = "ecode"; // 服務器返回的狀態
protected final int RESULT_CODE_VALUE = 0; // 服務返回的狀態值
protected final String ERROR_MSG = "emsg"; // 服務器返回的消息
protected final String EMPTY_MSG = ""; // 空消息
protected final String COOKIE_STORE = "Set-Cookie"; // decide the server it
// 自定義異常類型
protected final int NETWORK_ERROR = -1; // 網絡相關錯誤
protected final int JSON_ERROR = -2; // Json處理相關錯誤
protected final int OTHER_ERROR = -3; // 未知異常錯誤
/**
* 將其它線程的數據轉發到UI線程
*/
private Handler mDeliveryHandler; // 進行消息轉發
private DisposeDataListener mListener; // 自定義事件回調接口
private Class<?> mClass; // 要轉換的字節碼
接着實現Callback接口,然後就會實現onFailure()、onResponse()兩個方法。onFailure()就是我們請求失敗時調用的方法,例如: @Override
public void onFailure(final Call call, final IOException ioexception) {
/**
* 此時還在非UI線程,因此要轉發
*/
mDeliveryHandler.post(new Runnable() {
@Override
public void run() {
mListener.onFailure(new OkHttpException(NETWORK_ERROR, ioexception));
}
});
}
由於要將消息轉發到Ui線程中去更新界面,所以更新是會拋異常的。這裏我們使用handler去通知更新,同時這裏我們還用到了剛剛自定義的回到方法onFailure()方法,將異常信息傳入即可。接下來就該OnResponse()方法了 @Override
public void onResponse(final Call call, final Response response) throws IOException {
final String result = response.body().string();
mDeliveryHandler.post(new Runnable() {
@Override
public void run() {
handleResponse(result); // 處理從接口返回的數據
}
});
}
這裏是處理網絡請求成功之後的操作,說先就是要拿到服務器返回的數據。然後將這些數據轉發到主線程中。這裏我又單獨寫了一個處理數據的方法,就是將Json數據轉化成實體對象。
/** * 處理服務器返回的響應數據 * @param responseObj */ private void handleResponse(Object responseObj) { // 爲了保證代碼的健壯性 if (responseObj == null || responseObj.toString().trim().equals("")) { mListener.onFailure(new OkHttpException(NETWORK_ERROR, EMPTY_MSG)); return; } try { /** * 協議確定後看這裏如何修改 */ JSONObject result = new JSONObject(responseObj.toString()); // 開始嘗試解析json if(result.has(RESULT_CODE)){ // 從json對象中取出我們的響應碼,若爲0,則是正確的響應(根據服務狀態碼來定) if (mClass == null) { // 不需要解析,直接返回數據到應用層 mListener.onSuccess(result); } else { Gson gson = new Gson(); Object obj = gson.fromJson(responseObj.toString(),mClass); // 表明正確的轉化成實體對象 if (obj != null) { mListener.onSuccess(obj); } else { // 返回的不是合法的json mListener.onFailure(new OkHttpException(JSON_ERROR, EMPTY_MSG)); } } }else { // 將服務器返回我們的異常回調到應用層去處理 mListener.onFailure(new OkHttpException(OTHER_ERROR,result.get(RESULT_CODE))); } } catch (Exception e) { mListener.onFailure(new OkHttpException(OTHER_ERROR, e.getMessage())); e.printStackTrace(); } }
大致梳理一下上面的代碼,首先就是要判斷服務器返回的數據是否有效,無效的話直接調用咱們封裝的onFailure()。然後根據服務器返回的狀態碼是否是成功的。接着就是要處理Json數據,就是將Json數據轉化成實體對象.這裏我們可以使用第三方的Gson庫,直接在項目的build.gradle中引入就好了
compile 'com.google.code.gson:gson:2.4'
轉化完後再進一步驗證自己的轉化後的結果,保證把正確的結果傳遞到UI線程中好了,現在最後一個CallBack模塊也封裝好了,還記得剛剛我們在前面實現的Get請求可以優化嗎,這裏我將封裝好的CallBack放進去看看。優化後如下
/**
* 通過構造好的Get Request,Callback去發送請求
* @param request
* @param handle
* @return Call
*/
public static Call get(Request request, DisposeDataHandle handle){
Call call = mOkHttpClient.newCall(request);
call.enqueue(new CommonJsonCallback(handle));
return call;
}
優化後接受到數據後可以預處理好多數據,大大簡化我們後面我們調用時的操作。好了封裝思路圖裏的東西我們都已經實現,接下來我們要做最後一層封裝。由於我們的請求操作很多,例如註冊、登錄、詳情頁、支付等,爲了簡化我們的請求操作,我們還可以對上述已經封裝好的框架在封裝一次,當然這裏的封裝的目的就是簡化操作作爲API用,以後好統一管理。這裏我建立一個叫RequestCenter的類,我以登錄請求爲例,在RequestCenter的類中寫下如下方法:
//根據參數發送所有post請求 public static void postRequest(String url, RequestParams params, DisposeDataListener listener, Class<?> clazz) { CommonOkHttpClient.get(CommonRequest. createGetRequest(url, params), new DisposeDataHandle(listener, clazz)); } /** * 用戶登陸請求 * * @param listener * @param userName * @param passwd */ public static void login(String userName, String passwd, DisposeDataListener listener) { RequestParams params = new RequestParams(); params.put("mb", userName); params.put("pwd", passwd); RequestCenter.postRequest(HttpConstants.LOGIN, params, listener, User.class); }這個類還可以擴展,以後需要註冊請求、下載文件等方法都可以放到這個類中。最後咱們實際調用時是什麼樣子的
RequestCenter.login(userName, password, new DisposeDataListener() {
@Override
public void onSuccess(Object responseObj) {
DialogManager.getInstnce().dismissProgressDialog();
/**
* 這部分可以封裝起來,封裝爲到一個登陸流程類中
*/
User user = (User) responseObj;
UserManager.getInstance().setUser(user);//保存當前用戶單例對象
........
}
@Override
public void onFailure(Object reasonObj) {
DialogManager.getInstnce().dismissProgressDialog();
}
});
}
現在看看這個網絡請求的邏輯是不是相當乾淨簡潔。這就是封裝的好處。花了兩天的時間斷斷續續的總算寫好了,這期間也查閱了很多資料,學習了很多知識。由於本人水平有限,若有大神發現錯誤,還請幫忙指正,非常感謝。