Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android網絡請求庫 - Say hello to Volley

Android網絡請求庫 - Say hello to Volley

編輯:關於Android編程

書接上篇 《Android網絡請求庫 - Say hello to OkHttp》,今天接著來簡單的看一下常用的網絡請求庫中的第二種庫:Volley。

Volley是谷歌2013年在I/O大會期間推出的網絡庫。開發Volley是因為在Android SDK中缺乏一個用戶體驗良好的網絡加載類。 Volley自身的特點在於:適合去進行數據量不大,但通信頻繁的網絡操作;而對於大數據量的網絡操作,例如上傳/下載文件,其表現就不怎麼給力了。

好的,這裡就不多說廢話了。下面馬上正式走進關於Volley這個庫的使用。

導入Volley

有趣的一點是,與其它很多常見的開源庫的導入不同,Volley自身是沒有提供官方的Maven或者說Jcenter Repository的。
也就是說,如果我們想要使用這個庫,通常來說必須依賴於官方提供的源碼。所以我們首先需要把它的代碼clone到本地:

git clone https://android.googlesource.com/platform/frameworks/volley 

之前可能更多時候是將代碼clone後然後打包成jar文件進行導入使用,但隨著Volley的發展以及AndroidStudio的普及。我們現在更好的做法是:
將下載好的源碼作為一個module導入。在Android Studio中選擇File > Import Module,在對應目錄下找到下載好的Volley項目,選擇好後點擊確認。
之後一個名為Volley的文件夾將出現在項目結構中。AS會自動的更新settings.gradle文件以包含Volley module,因此我們只需要在自己的項目添加好依賴compile project(‘:volley’)就搞定了。

但或多或少因為這樣那樣的原因,例如大天朝的“高牆”或者說作為一個懶人,我們可能不會太喜歡這種方式。沒關系,也有另一種更簡單的方法:

compile 'com.mcxiaoke.volley:library:1.0.19'

很顯然通過這種方式引入Volley要方便很多,但缺陷在於:前面也說到了這不是官方提供的repository,也就是說它的未來是不可預知的,這點很致命。
與此同時,另一個缺陷在於:對於開源庫來說其實最大的優勢就是在於開源,這意味我們雖然是作為使用者,卻仍然擁有最大的自由度。我們可以根據自己的需求最大限度的去自定義和擴展庫,而使用這種方式則意味著可發揮的空間小了很多。

 

基本使用

HTTP GET

與之前學習OkHttp的思路保持一致,對於了解Volley的使用,我們同樣從發起一個最基本的GET請求開始。

        // 第一步
        RequestQueue queue = Volley.newRequestQueue(this);
        // 第二步
        StringRequest request = new StringRequest(url, new Response.Listener() {
            @Override
            public void onResponse(String response) {
                Log.d(TAG,response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG,error.toString());
            }
        });
        // 第三步
        queue.add(request);

依然是在本地使用tomcat搭建一個簡單的servlet服務器,這次get請求如果成功,服務器將返回“get請求成功了,逗逼!”的信息。
好的,那麼還有什麼好等的呢?現在我們就運行程序來驗證一下吧,最後發現得到如下圖所示的輸出結果:
這裡寫圖片描述
所以我們可以確定我們已經成功的通過Volley發送了一條get請求到服務器了。簡單的總結一下:

從目前看來,對於Volley的使用相比於OkHttp更加簡單,因為通常我們基本就使用兩個東西:RequestQuene和Request。
而與此同時,就從命名上來說,我們也可以很容易的理解它們各自的作用。顯然二者分別代表請求隊列以及請求。
(這裡的StringRequest其實就是基類Request的派生,顧名思義也就是針對於那些響應類型為文本的請求做了單獨的封裝。對應的常見的請求類型有三種:StringRequest、ImageRequest、JsonRequest) 接著,我們會注意到隊列這個東西,相信Queue這個命名會條件反射一樣的讓我們與“多個數量”聯系起來。
實際上事實也是如此,RequestQueue會將多個HTTP請求緩存進隊列,然後按照一定的算法分別去執行這些請求。
所以,通常我們很顯然沒有必要將這個東西在每次需要發起請求時都去new一次,可以將RequestQueue設計為單例來節約資源。 同時我們可以看到在Volley中發起一個請求的操作還是比較簡單,只需要如上面示例的代碼中的三步操作即可。
簡單的概括來說,就是得到RequestQueue與Request對象,然後將request添加到請求隊列中就可以了。

HTTP POST

看完了GET請求的使用,自然就輪到了POST請求了。POST請求在Volley中也很容易實現。以之前的StringRequest來說:
其構造器提供了兩種方式,我們之前使用了一種。而另一種多了一個int型的參數,這個參數就是來區分請求方法的。

StringRequest request = new StringRequest(Request.Method.POST,url, new Response.Listener() {

這裡寫圖片描述
我們發現,我靠?這麼簡單。當然不是,因為實際來說肯定不會出現如此基礎的POST請求。例如我們通常起碼會在POST請求中上傳一些參數值吧。
在Volley中我們該如何去做呢?我們當然會從StringRequest入手,但最後會發現該類暴露出的實例方法基本全是一些get相關的方法,沒有我們期望的。
這個時候實際上是挺讓人蛋疼的,那既然Volley原本沒有提供給我們現成的東西,那我們就只能自己動手了。其實也不復雜,覆寫getParams方法吧。

        StringRequest request = new StringRequest(Request.Method.POST, url, new Response.Listener() {
            @Override
            public void onResponse(String response) {
                Log.d(TAG, response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.d(TAG, error.toString());
            }
        }) {
            @Override
            protected Map getParams() throws AuthFailureError {
                Map map = new HashMap<>();
                map.put("params1", "value1");
                map.put("params2", "value2");
                return map;
            }
        };

不知道你怎麼想,反正個人是覺得Volley通過這樣的方式來完成諸如之類的操作,使用起來其實還是有點蛋疼的。
但也無所謂吧,我們把之上的操作當做起步,有了這個基礎,我們很容易舉一反三。例如我們現在想要添加請求頭信息:

            @Override
            public Map getHeaders() throws AuthFailureError {
                Map map = new HashMap<>();
                map.put("header1", "呵呵");
                map.put("header2", "哈哈");
                return map;
            }

我們這裡的重點在於掌握其套路,因為“開源“,類似功能的使用或者拓展在,在掌握套路的基礎上結合研究源碼通常就不難找到答案了。
所以,關於Volley中GET與POST的基本使用,我們只點到為止,接下來看一些更加有意思一點的東西。

 

徹底理解Volley中的亂碼問題

回憶下,我們在此前的基本使用的介紹中,GET與POST請求的中文響應信息都成功的打印在了日志中。
而我相信我們都可能在自己使用時碰到過服務器返回的中文信息顯示亂碼的問題。這在之前看郭神寫的介紹Volley的系列文章中也看到人提起過:

這裡寫圖片描述
實際上正如這張圖裡的朋友說的,只要重寫parseNetworkResponse就可以了。解決問題的方法不難找到,而我們這裡的關注點放在出現問題的原因上。

好的,那麼現在我們來看一下為什麼在之前的例子中服務器響應中的中文信息能夠成功輸出呢?首先我們看服務器的實現:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 設置編碼
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        // 輸出響應
        OutputStream out = response.getOutputStream();
        out.write("get請求成功了,逗逼!".getBytes("UTF-8"));
        out.flush();
        out.close();
    }

顯然我們做的工作是將服務器的響應信息設定了編碼格式,這裡是UTF-8,而為什麼這樣做Volley就不會出現讀取亂碼了呢?
答案實際上就在之前說到的parseNetworkResponse方法中,讓我們打開StringRequest類中這個方法的源碼看一下:

    protected Response parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

我們發現這個方法的邏輯實際上是十分簡單的,很快我們的關注焦點就放在了另一個方法HttpHeaderParser.parseCharset上,同樣的打開它的源碼:

    public static String parseCharset(Map headers, String defaultCharset) {
        String contentType = headers.get(HTTP.CONTENT_TYPE);
        if (contentType != null) {
            String[] params = contentType.split(";");
            for (int i = 1; i < params.length; i++) {
                String[] pair = params[i].trim().split("=");
                if (pair.length == 2) {
                    if (pair[0].equals("charset")) {
                        return pair[1];
                    }
                }
            }
        }

        return defaultCharset;
    }

可以看到這個方法的實現其實也很簡單,實際就是在解析我們之前在服務器的response中設置的Content-Type,最終得到設置的charset,即編碼格式。
當該方法完成解析,得到我們服務器設置在response header中的charset信息之後,通過new String(byte[],”UTF-8”)就讓我們成功的避免了亂碼。

為了讓我們加深對這種錯誤的印象,我們可以再逆向的思維一下,假設我們在服務器中沒有設置編碼格式:

        OutputStream out = response.getOutputStream();
        out.write("get請求成功了,逗逼!".getBytes());
        out.flush();
        out.close();

這個時候,我們再次運行程序就會發現得到的響應信息出現了亂碼,這個時候我們該如何去解決呢?其實了解了原理就很簡單了:

            parsed = new String(response.data, "gb2312");
            //parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));

我們所做的只是簡單修改了parseNetworkResponse中的一行代碼。那麼為什麼這裡我們修改成“gb2312”就可以了呢?因為response.getOutputStream()的默認編碼格式就是gb2312。所以說穿了避免中文亂碼的關鍵很簡單,就是和服務器統一編碼格式。

接下來,我們再來看另一種情況,我們改用PrintWriter來返回響應信息:

        PrintWriter out = response.getWriter();
        out.write("get請求成功了,逗逼!");
        out.flush();
        out.close();

這個時候情況其實更有意思一點,因為這個時候我們如果不明確的指定編碼格式,那麼亂碼問題是無解的。為什麼這麼說呢?我們分析一下:
response.getWriter()的默認編碼格式是什麼呢?是ISO-8859-1。而StringRequest的默認解碼格式呢?答案同樣是ISO-8859-1。參照以下源碼:

    public static String parseCharset(Map headers) {
        return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
    }
    // ================================================
    public static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";

那麼,現在的情況乍看一下是:默認情況下服務器和客戶端的編碼格式實際上是統一的,那為什麼還會出現亂碼,甚至還說這時的亂碼是無解的呢?因為我們說的是中文亂碼。ISO-8859-1的編碼格式與中文響應,你懂的!

不要說,越深入越帶勁了嘿!那我們之前說的POST請求上傳參數時,會不會出現亂碼呢?當然也是可能出現的。但為什麼我們之前沒有出現亂碼呢?
在源碼中找答案,這個答案並不神秘,就在基類Request當中,首先是如下代碼:

    /**
     * Returns the content type of the POST or PUT body.
     */
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }

這裡首先解釋了,為什麼我們之前只重寫了getParams方法就可以上傳參數了,因為Request類裡默認為我們指明了Content-Type為表單形式。
與此同時,我們注意到:getParamsEncoding()方法為Content-Type指明了編碼格式,打開該方法的源碼吧:

    protected String getParamsEncoding() {
        return DEFAULT_PARAMS_ENCODING;
    }

    private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";

話到這裡,其實就很清楚了。關於Volley中的亂碼相關的東西,就總結到這裡吧。

 

其它兩種Request

我們之前知道了在Volley中Request是對應於請求的基類,而常用的Request類型有三種,除了StringRequest,還有JsonRequest與ImageRequest。
其實這種情況實際只是Volley針對於不同的響應數據類型單獨做了最合適的封裝,這也是為什麼之前說Volley相對於OkHttp的優勢在於封裝度更高。
因為原理是相通的,所以其實對於另外兩種的Request的使用方式其實也是類似的,不過我們也可以再看一下。

JsonRequest

JsonRequest顧名思義,就是說服務器響應的數據類型是JSON格式的,而對於其使用,與StringRequest是很相似的:

        JsonObjectRequest request = new JsonObjectRequest(url, new Response.Listener() {
            @Override
            public void onResponse(JSONObject response) {

            }
        },null);

可以看到與StringRequest最大的不同就是Listener的泛型變為了JSONObject,進而onResponse返回的也就直接是JSONObject了。
而對於JSON數組來說,使用同樣簡單,把這裡的JsonObjectRequest改為使用JsonArrayRequest就可以了。

ImageRequest

從名字我們可以看到這個類顯然就是為了圖片專門封裝的,相比StringRequest和JsonRequest稍有不同了。(JSON就只是指定了格式的文字而已)
但也是由於Volley本身封裝的比較好,所以雖然不再是響應文字而是響應圖片,但使用起來也很簡單:

        ImageRequest request = new ImageRequest(url, new Response.Listener() {
            @Override
            public void onResponse(Bitmap response) {
                imageView.setImageBitmap(response);
            }
        },maxWidth,maxHeight,scaleType,decodeConfig,errListener);

我們發現ImageRequest接收的參數比較多,但歸根到底這些多出來的參數實際也就是為了設置圖片的相關屬性而存在的。

maxWidth //圖片最大寬度 maxHeight//圖片最大高度 scaleType //指定圖片縮放類型 decodeConfig//指定圖片的顏色屬性

對於圖片的下載顯示,Volley還提供了一個相較ImageRequest更加強力一點的東東ImageLoader,關於其使用其實也不復雜,但畢竟Volley並不是一個將精力聚焦在圖片加載的庫,所以關於ImageLoader如果有興趣我們可以自己另行研究。

 

定制自己的Request

前面我們了解了Volley自身提供了三種常用的Request類型,但同時我們也可以根據自定的需要定制自己的Request類型。在Volley中去自定義自己的Request其實並不復雜,以StringRequest來說,讀一下其源碼,就會發現邏輯是很簡練清晰的。我們可以以此為參考,根據自己的需要進行修改,很容易就能寫出自己需要的Request。而以Http請求來說:我們除了上面三種之外,還有XML格式。而Volley自身提供的對於JSON格式的數據的響應是基於JSONObject(Array)的,很多時候我們還是喜歡GSON和fastJson。所以我們就可以去定制基於XML或者GSON的Request類型,而對於這種需求,我們借以學習就可以了。

另一些常用的操作

取消請求

取消請求是比較簡單,根據不同的需要有以下幾種方式:

        //第一種
        request.setTag("tag");
        queue.cancelAll("tag");
        //第二種
        queue.cancelAll(new RequestQueue.RequestFilter() {
            @Override
            public boolean apply(Request request) {
                //通過該返回結果決定該request是否需要終止
                return true;
            }
        });
        //第三種
        request.cancel();

Request優先級

我們知道我們在使用Java的線程的時候,可以通過setPriority來設置線程優先級。而對於Volley中的Request,也可以管理它們的優先級。
不同之處在於,Request類本身沒有提供setPriority,而只是提供了getPriority方法而已:

    public Priority getPriority() {
        return Priority.NORMAL;
    }

但沒關系,知道了原理我們就可以自己加以修改,從而自己提供一個setPriority的入口,類似下面這樣重寫Request類:

Priority mPriority;

public void setPriority(Priority priority) {
    mPriority = priority;
}

@Override
public Priority getPriority() {
    // If you didn't use the setPriority method,
    // the priority is automatically set to NORMAL
    return mPriority != null ? mPriority : Priority.NORMAL;
}

Priority是定義在Request類內部的一個枚舉類型,它有以下幾種取值可能:

    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }
 

宏觀的看Volley的架構

到了現在,我們已經對Volley這個庫的使用有了一定程度的認識。現在我們通過Volley的源碼從更宏觀的角度來看一下其原理,加深我們的理解。

回憶一下,我們在使用Volley發起一個請求時所需的步驟。顯然首先我們需要一個RequestQueue對象:

RequestQueue queue = Volley.newRequestQueue(this);

我們就從這行代碼作為起點,看一下它的源碼實現是如何的,這行代碼最後將進入如下調用:

    public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        /*
         * 第一部分
         * 緩存文件相關信息的初始化設置
         */
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        String userAgent = "volley/0";
        try {
            String packageName = context.getPackageName();
            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
            userAgent = packageName + "/" + info.versionCode;
        } catch (NameNotFoundException e) {
        }

        /*
         * 第二部分
         * 實例化HttpStack對象,這裡有一個版本判斷。
         * 如果版本高於9,則創建的是HurlStack對象,其內部是通過HttpUrlConnection實現的。
         * 而如果版本低於9,則創建的是HttpClientStack對象,其內部是通過HttpClient實現的。
         */
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        /* 第三部分
         * 創建Network對象,並進行RequestQueue的實例化工作。
         * (BasicNetwork實際就是根據傳入的HttpStack在內部處理網絡請求的東西)
         * (maxDiskCacheBytes是指定磁盤緩存的最大容量,指定為-1時將使用默認大小'5 * 1024 * 1024')。
         *
         */
        Network network = new BasicNetwork(stack);

        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            // No maximum size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            // Disk cache size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        /*
         * 讓RequestQueue開始工作
         */
        queue.start();

        return queue;
    }

我們對以上的代碼所住的幾部分主要工作做了劃分,並加以注釋進行了說明。可以發現邏輯其實還是很清楚的,現在我們下一步的注意力放在queue.start():

    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

我們發現start方法的源碼其實也不復雜,最關鍵的兩個東西分別是:CacheDispatcher和NetworkDispatcher,它們其實都繼承自Thread。
而從命名,我們就可以發現它們分別對應著緩存線程與網絡線程,而我們發現網絡線程包含於一個for循環當中,mDispatchers.length的默認值是4。
這意味著當RequestQueue執行start過後,默認情況下我們的應用中就會有1個緩存線程及4個網絡線程待命進入工作,等待著執行我們的請求。

到了這裡,我們就得到了RequestQueue對象了。下一部的工作我們也很熟悉,就是將request對象add進行隊列。那麼啥也別說了,看add方法的源碼吧:

 public  Request add(Request request) {
        // 標記屬於此隊列的請求,並將其添加到當前請求的集合中。
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }

        // 按添加順序為其設置序列
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        // 如果request不允許緩存,將其加入網絡隊列
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

        /*
         * 簡單的來說就是指:如果已經有擁有相同cacheKey的請求存在於mWaitingRequests當中了,則將本次請求添加進stagedRequests當中
         */
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList>();
                }
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
            } else {
                // 否則以cacheKey為鍵,插入null對象作為值,並將本次請求加入緩存隊列。
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

我們發現,最終通常來說request就被加入到了緩存隊列當中,那麼就意味著要CacheDispatcher開始執行了,所以我們看看它的run()方法吧:

 @Override
    public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 緩存的初始化工作(mCache的實際類型是DiskBasedCache)
        mCache.initialize();

        Request request;
        while (true) {
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // 從緩存隊列中獲取request對象
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // 找不到緩存的話,則將請求加入到網絡隊列中去執行
                    mNetworkQueue.put(request);
                    continue;
                }

                // 緩存過期了的話,同樣加入到網絡隊列中去執行
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // 否則就代表緩存可以使用,直接從緩存讀取數據
                request.addMarker("cache-hit");
                Response response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 緩存不需要更新,將從緩存中讀取的response通過mDelivery發送回去
                    mDelivery.postResponse(request, response);
                } else {
                    // 否則雖然我們仍然可以使用緩存中讀取到的響應返回
                    // 但同時還要進行緩存的更新工作。
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    final Request finalRequest = request;
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
            }
        }
    }

通過對以上代碼的理解我們可以知道,假設我們第一次發送某個請求,在緩存線程中執行後,當然會因為沒有緩存而被加入到網絡線程。
所以接下來我們要關注的當然就是NetworkDispatcher的run方法,而通過查看源碼我們會發現其代碼與CacheDispatcher有些相似的,不同在於:
NetworkDispatcher判斷Request可以執行後,就會調用mNetwork.performRequest(request);開始實際執行HTTP請求。
關於performRequest的源碼,有興趣的可以自己查看,其實際實現位於BasicNetwork當中。代碼很長,由於篇幅就不貼出了。
而當網絡線程與服務器完成通信,就會得到響應(networkResponse);這之後做的工作很簡單也很重要,大致分為三步:

通過Response< ? > response = request.parseNetworkResponse(networkResponse);將讀取到的網絡相應於根據響應類型進行具體轉換。 正常情況下,還會通過mCache.put(request.getCacheKey(), response.cacheEntry);將本次響應進行緩存。 最後調用mDelivery.postResponse(request, response);將響應傳回給客戶端。

相信到了這裡,我們就在宏觀的角度上對於Volley的整個結構有了一個基本的認識了。

 

為什麼沒法使用緩存

本來已經准備結束了,突然想起關於Volley中文本類型(包括JSON等)的數據的緩存其實也值得一說。
我們來回憶一下,Volley的緩存機制是怎麼樣的。其實分別有幾個關鍵點。我們首先回一下RequestQueue類裡add方法的幾行代碼:

        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

這個判斷的含義告訴我們說,如果request.shouldCache()返回為false,即代表不需要緩存。則會將此次請求直接加入網絡隊列執行。
否則的話,一個請求通常最後都會被加入到mCacheQueue即緩存隊列中執行,而request.shouldCache()的默認返回值為true,即代表默認支持緩存。

接著,當request加入到緩存隊列開始執行後,如果因為此時還沒有緩存,就會加入到網絡隊列中執行,而得到響應後,會通過如下代碼保存緩存:

                if (request.shouldCache() && response.cacheEntry != null) {
                    Log.d("TestVolley","put in cache");
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

這個判斷默認情況下也是會通過的,也就是說默認情況下我們的響應都是會進行緩存的。

而在回憶一下,我們同樣記得,在CacheDispatcher中,是會通過如下代碼去獲取緩存的:

 Cache.Entry entry = mCache.get(request.getCacheKey());

我們可以自己去寫代碼測試一下,就會很容易的發現當一次請求成功後。再次發起相同請求時,entry的獲取肯定是不會null的,但問題在於:
此次發起的請求卻不是讀取緩存,而是再次通過網絡線程去發起新的請求。這是為什麼呢?其實已經不難猜到答案了。問題多半出在如下判斷:

if (entry.isExpired()) {

我們可以看一下該方法的源碼是如何實現的:

        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

從而我們會發現一個關鍵的信息叫做ttl,當我們繼續深入去查看這個ttl,會發現默認情況下它是為0的。也就是說,默認情況下isExpired的返回結果肯定是為true的。
這也就是為什麼我們其實有緩存,卻沒法使用緩存。那麼怎麼樣才能讓這個值不為0呢?很簡單,這個值的計算是與HTTP的一個header屬性,也就是“Cache-Control”相關的。

現在我們在服務器的加上類似代碼:

response.setHeader("Cache-Control", "max-age=3600");

OK,搞定收工。有了這行代碼,當我們再次去發起該請求的時候,只要沒有超過max-age指定的時間,我們就可以復用緩存中的響應,避免新的請求了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved