1. 程式人生 > >Android應用開發:網路工具——Volley(一)

Android應用開發:網路工具——Volley(一)

引言

網路一直是我個人的盲點,前一陣子抽出時間學習了一下Volley網路工具的使用方法,也透過原始碼進行了進一步的學習,有一些心得想分享出來。在Android開發中,成熟的網路工具不少,Android自帶了HttpClient,還有okhttp,還有koush大神建立的ion開源專案,然後就是google後來加入到Android專案原始碼中的Volley。為什麼使用Volley,是因為Volley使用簡單,邏輯清晰,即使在除錯過程中出現了問題,也可以快速的通過原始碼進行定位。

Volley編譯

因為已經習慣了使用Gradle構架應用,所以我在第一次想要使用Volley的時候嘗試尋找是否可以通過gradle的配置檔案進行庫依賴。可惜的是,並沒有。但即使這樣Volley的庫也很容易做出來加入到我們的工程中。

首先需要ant編譯工具,然後如果有Android系統原始碼的話,Volley在frameworks/volley目錄下。如果沒有Android原始碼,也很好辦,可以單獨從Android的倉儲中克隆出Volley原始碼:

git clone https://android.googlesource.com/platform/frameworks/volley
不幸的是,volley庫的原始碼Android並沒有託管在其在Github的帳號上,所以只能在googlesource上進行克隆,當然在國內也就需要先FQ才可以了。

下圖為Volley原始碼結構:


克隆成功後,可以方便的使用ant進行編譯,當然,如果是在完整的Android原始碼下,也可以直接通過make進行編譯,但是時間必然會長很多。這裡使用ant編譯為例,執行:

ant jar
結果如圖所示:


這樣jar包就生成了,很方便吧,接下來將其新增到工程中就可以使用了。

Volley使用

Volley的網路請求父類為Request<T>,可以提供給開發者進行繼承,同時也預置了幾種開發中常用的請求型別,下邊介紹兩個:StringRequest和JsonObjectRequest。

為了更加貼近實際使用,下邊將使用Volley與Cloudant進行通訊做示例。Cloudant是一家提供雲服務業務的公司,其向開發者提供免費的雲端儲存、雲資料庫服務。關於其註冊等流程本文不做敘述,很簡單的。直接從登入開始:

1. 申請網路請求佇列

Volley的一個很大的特色,就是所有的網路請求無需開發者自己執行,而是在請求構造完成後扔到Volley的請求佇列中,佇列依次進行請求,這樣就省去了很多麻煩,開發者也不用擔心網路請求是否會衝突,是否會在主執行緒,這些煩心事Volley的網路佇列都幫我們解決了。

一般來說,一個應用程式如果網路請求沒有特別頻繁則完全可以只有一個請求佇列(對應Application),如果非常多或其他情況,則可以是一個Activity對應一個網路請求佇列,具體情況具體分析。下邊的程式碼展示瞭如何申請一個Volley網路請求佇列:

RequestQueue mQueue;
mQueue = Volley.newRequestQueue(getApplicationContext());

這樣就成功申請了一個網路請求佇列,如果只有一個,則可以在Application中進行申請。

2. 使用Volley登入Cloudant

假設已經成功註冊,登入名foo,密碼bar。

通過查閱Cloudant的登入認證文件:https://docs.cloudant.com/api/authn.html。可以發現Cloudant登入認證相關介面有三個:


這裡我們使用POST方法進行cookie登入認證。結合上邊假設的使用者名稱和密碼可知:

要訪問的url為 foo.cloudant.com/_session
頭資訊為 Content-Type: application/x-www-form-urlencoded
引數為 name = foo, password = bar

若訪問成功,我們就可以在網路迴應中獲取cookie,以備之後其他操作使用。顯然,這個請求跟json毫無關係,應該使用StringRequest,StringRequest有兩種構造方法:

public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener)

public StringRequest(String url, Listener<String> listener, ErrorListener errorListener)
第二個方法只有GET請求才可以使用,第一個方法的method引數可以用來自定義請求型別,這裡我們需要的是POST,所以應該使用第一個構造方法:
StringRequest request = new StringRequest(
                Request.Method.POST,
                "http://foo.cloudant.com/_session",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String s) {  //收到成功應答後會觸發這裡

                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) { //出現連線錯誤會觸發這裡
                    }
                }
        );

上邊的程式碼中,我們成功構造了一個StringRequest,其中已經包含了我們需要的POST和正確的URL,同時還添加了網路迴應監聽器。但是,還缺少文件要求我們的頭資訊和引數。StringRequest在構造中並不提供這些資訊的定義,這也是與其他常用網路工具不同的地方,剛接觸的同學可能會很不適用,通過複寫StringRequest的兩個方法就可以將這些資訊放進去了。下邊來完善這個請求:

StringRequest request = new StringRequest(
                Request.Method.POST,
                "http://foo.cloudant.com/_session",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String s) {

                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {

                    }
                }
        ) {
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {  //設定頭資訊
                Map<String, String> map = new HashMap<String, String>();
                map.put("Content-Type", "application/x-www-form-urldecoded");
                return map;
            }

            @Override
            protected Map<String, String> getParams() throws AuthFailureError {  //設定引數
                Map<String, String> map = new HashMap<String, String>();
                map.put("name", "foo");
                map.put("password", "bar");
                return map;
            }
        };

相比第一次我們的構造過程,這一次多了兩個複寫的方法來設定頭資訊和引數,很容易吧。這個時候請求基本完成了,但是卻缺少另一個很重要的東西,我們的登入認證為的是拿回屬於自己的cookie,如果不能獲取cookie的話,多麼正確的請求格式都是白費力氣啊,想要拿到cookie一樣也是通過複寫另一個方法進行獲取:

            @Override
            protected Response<String> parseNetworkResponse(NetworkResponse response) {
                for (String s : response.headers.keySet()) {
                    if (s.contains("Set-Cookie")) {
                        mCookie = response.headers.get(s);
                        break;
                    }
                }
                return super.parseNetworkResponse(response);
            }
在網路請求成功後,服務端返回應答資訊,而我們所需的Cookie資訊就在這些應答資訊中,通過對應答資訊的遍歷查詢,很方便就可以找到我們所需的資訊了。到這裡,我們的登入認證請求就構造完成了,最後需要做的就是將這個StringRequest扔到我們的請求佇列中去:
mQueue.add(request);
網路通暢的情況下,很快就能夠獲取Cookie資訊了。

3. 檢視測試文件

在註冊Cloudant成功後,Cloudant會在我們的帳號中建立一個預設資料庫——crud,其中儲存著一行測試資料welcome。


讓我們用Volley來訪問這條資料。查閱Cloudant API文件Documents相關可以發現:


通過簡單的GET請求搭配正確的URL即可得到檔案(資料)內容,當然,這一切的前提是我們已經掌握了正確的Cookie資料。那麼,我們需要:

1. 請求頭資料中包含正確的Cookie資訊
2. 訪問正確的URL
3. 請求型別:GET
假設通過上一步登陸認證後我們將Cookie資訊儲存在了mCookie字串變數中。而我們需要訪問的URL通過查閱文件也可以得出路徑為 資料庫名 + 文件名,即foo.cloudant.com/crud/welcome。萬事俱備,使用StringRequest:
        StringRequest request = new StringRequest(
                "http://foo.cloudant.com/crud/welcome",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String s) {

                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {

                    }
                }
        ) {
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> map = new HashMap<String, String>();
                map.put("Cookie", mCookie);
                return map;
            }
        };
        mQueue.add(request);

在onResponse中我們會收到welcome這條資料的json形式字串:

簡單的網路請求StringRequest完全處理得來,使用也比較簡單,就介紹到這裡。下邊介紹JsonObjectRequest應用方法。

4. 使用JsonObjectRequest建立新資料

首先看一下JsonObjectRequest的構造方法:
public JsonObjectRequest(int method, String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener)

public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener)
第一種方法引數以此為:請求方法,訪問的URL,Json資料物件,請求成功監聽器,請求失敗監聽器。 第二種構造方法中,若jsonRequest為空,則方法自動為GET,不為空則自動切換為POST,其他引數含義相同。

Cloudant的文件(https://docs.cloudant.com/api/documents.html)要求建立文件可以使用POST或PUT方法進行,所攜帶的資料均為json格式。這樣以來,StringRequest就顯得力不從心了,我們需要使用到Volley的另一個自帶請求型別:JsonObjectRequest。下邊以POST方式建立資料為例,通過檢視Cloudant文件,可知:
1. 訪問的URL path為資料庫目錄
2. Content-Type被要求為application/json
3. 攜帶的資料要求為json資料

既然方法要求為POST,我們又是建立資料,肯定資料內容不會為空,所以我們選擇第二種構造方法。首先,建立一個Json物件:

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("_id", "testinfo");
        jsonObject.put("person", "foo");
        jsonObject.put("phone", "bar");

在Cloudant資料儲存系統中,id可以由開發者指定。接下來進行JsonObjectRequest的構造和請求:
        JsonObjectRequest request = new JsonObjectRequest(
                "http://foo.cloudant.com/crud",
                jsonObject,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject jsonObject) {

                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {

                    }
                }
        ) {
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> map = new HashMap<String, String>();
                map.put("Cookie", mCookie);
                return map;
            }
        };
        mQueue.add(request);

jsonObject資料不為空,所以請求方式自動切換為POST,url為所要建立資料所在的資料庫所在路徑,然後就是請求結果的監聽器,最後別忘了將Cookie帶上,否則會出現認證錯誤的。最後,將構造完成的請求丟進佇列中,由Volley進行排程處理。這個時候不妨再回頭看一看之前分析的請求所需要哪些元素,不難發現,Volley的json請求中,並沒有對Content-Type進行特殊設定。JsonObjectRequest是繼承於JsonRequest的,而JsonRequest已經幫我們完成了這個動作:
    @Override
    public String getBodyContentType() {
        return PROTOCOL_CONTENT_TYPE;
    }

PS:設定Content-Type也可以通過複寫getBodyContentType這個函式,而不用總是麻煩的使用getHeader中的map進行設定,兩種設定方式效果一致。而且也不用擔心編碼格式,因為預設就是utf-8的:
    /** Charset for request. */
    private static final String PROTOCOL_CHARSET = "utf-8";

    /** Content type for request. */
    private static final String PROTOCOL_CONTENT_TYPE =
        String.format("application/json; charset=%s", PROTOCOL_CHARSET);

到這裡,Json請求的相關用法也就介紹完了。下一節將會從原始碼角度分析一下Volley請求的邏輯順序究竟是怎樣的,如果我們需要書寫自己的請求型別,都需要複寫哪些函式,以及需要注意些什麼。

原始碼

關於Volley和Cloudant更多的通訊細節,見CloudantVolley專案:https://github.com/airk000/CloudantVolley