天天看點

Volley網絡緩存詳解Volley網絡緩存詳解

Volley網絡緩存詳解

我為何會研究Volley中的網絡緩存?

因為我這裡做一個網絡操作,由于伺服器端實作實在是太垃圾,一個接口擷取資料需要一兩秒,然後産品需求是一次性需要查七次,那麼一次弄下來就會耗時10秒左右,但是如果有看過Volley代碼的朋友都會知道,Volley是預設4個線程同時從隊列中take出Request進行網絡請求的,理論上肯定是7個請求,隻用分兩次就會全部發出去的,但是實際操作中會發現隻會有一兩個線程在串行的進行網絡請求。是以這裡我花了一天的時間來仔細研究了Volley中的網絡緩存操作。頗有收獲,與大家分享一二,如有不完善或錯誤,請指出一起讨論。

如何解決本來應該并行但是确串行的網絡請求?

先說一下解決方案,一共兩種方法解決:

第一種:給Request對象設定不允許緩存,調用setShouldCache(boolean shouldCache)方法,Request中mShouldCache值預設為true,設定為false即可。

第二種:重寫Request中的getCacheKey()方法,為每一個請求設定不同的CacheKey,因為如果不重寫的話,這個方法預設傳回url作為CacheKey,而我之前每一個請求都是通路的同一個url,是以這裡修改之後能夠解決這個問題,當然為什麼需要修改,請繼續往下看。

這裡還有問題

上面說到的解決方法,第一種是不允許緩存,第二種是允許緩存,但是你實際上如果使用第二種方法的話,你會發現實際上你的緩存資料根本沒用的,也就是說,其實Volley給你寫到本地緩存上面了,但是讀取出來是無效的。這裡因為Cache.Entry.ttl值為0,這個值是緩存有效期,機關為毫秒,Entry中對緩存是否有效的判斷實作代碼如下:

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

如果小于目前系統時間,則為失效。這個ttl的值是Volley根據發出網絡請求的時間加上Response中header裡面cache-control: max-age的值相加得出的有效期,原本來說這裡的cache-control應該是從伺服器傳回的,但是實際應用中,很難找到背景開發人員專門為前端傳回相應的字段,是以這裡本地也應該要有解決方法,其實你應該也猜到需要怎麼做了,我們這裡隻要在本地得到Response後去寫入緩存這裡的過程中改變Response的header值即可。具體方法最簡單應該是在初始化Volley隊列的時候傳入自定義的HttpStack,繼承BasicNetwork,重寫方法如下:

@Override
      public NetworkResponse performRequest(Request<?> request) throws VolleyError {
          long maxAge = ;
          try {
               String age = request.getHeaders().get("max-age");
               if (!AntiDataUtils.isEmpty(age)) {
                   maxAge = Long.parseLong(request.getHeaders().get("max-age"));
               }
           } catch (Exception e) {
               e.printStackTrace();
           }
           NetworkResponse response = super.performRequest(request);
           List<Header> headerList = new ArrayList<>();
           headerList.addAll(response.allHeaders);
           headerList.add(new Header("Cache-Control", "max-age=" + maxAge));
           return new NetworkResponse(response.statusCode, response.data, response.notModified, response.networkTimeMs, headerList);
      }
           

Volley網絡緩存流程

首先需要了解Volley整體結構,Volley的整體結構簡單而巧妙,通過兩個同步隊列實作多線程的網絡請求。使用過Volley的朋友都應該了解,在使用Volley進行網絡請求之前,需要初始化一個RequestQueue,在RequestQueue對象中主要成員如下:

//取緩存的隊列,如果目前網絡請求需要緩存,則添加到此隊列
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
            new PriorityBlockingQueue<>();

    //取網絡結果的隊列,如果目前網絡請求不需要緩存則直接添加到此隊列
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
            new PriorityBlockingQueue<>();

    //發送網絡請求的線程數組,這代表支援多線程網絡,可以通過構造方法傳入數組長度,即為支援的線程個數,預設為4
    private final NetworkDispatcher[] mDispatchers;

    //取緩存的線程
    private CacheDispatcher mCacheDispatcher;
           

如果說是不用緩存的網絡請求,在放進隊列之後,這裡的流程很簡單,在NetworkDispatcher中會馬上被take出,取得網絡結果後給出回調,如果是需要緩存的網絡請求,這裡就會比較複雜一點。

首先是CacheDispatcher會先從mCacheQueue中take一個request,然後根據CacheKey(如果沒有則預設為url)讀取本地緩存:

  • 如果沒有緩存檔案,則Entry==null,在這時,會将request直接再放入mNetworkQueue,NetworkDispatcher會馬上去take出來進行正常的網絡請求。
  • 如果有緩存檔案:
    • 如果緩存有效,則回調緩存結果
    • 如果緩存過期,則将request放入 mNetworkQueue。

這裡是最簡單的邏輯,但是似乎會有問題,如果我同時放10個CacheKey(url)相同的request進來,那按照這樣的做法,如果這10個request都是可以緩存的資料,比如說查詢一個當日有效的結果,同時查詢幾次,其實這裡除了第一個需要從網絡取資料之外,其餘的都可以等待第一個request取到結果并寫入緩存後直接從緩存中讀取,這樣效率明顯會高很多。這樣一個常識性的操作,難道google大佬沒想到嗎?那答案肯定是否定的,大佬終究是大佬,早就實作這一點了:

//取得緩存資料
        Cache.Entry entry = mCache.get(request.getCacheKey());
        if (entry == null) {
            request.addMarker("cache-miss");
            // Cache miss,send off to the network dispatcher.
            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mNetworkQueue.put(request);
            }
            return;
        }

        // If it is completely expired, just send it to the network.
        if (entry.isExpired()) {
            request.addMarker("cache-hit-expired");
            request.setCacheEntry(entry);
            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mNetworkQueue.put(request);
            }
            return;
        }
           

這裡是CacheDispatcher中的操作,這裡的關鍵就在于mWaitingRequestManager.maybeAddToWaitingRequests(request)這一句代碼,如果之前還有相同CacheKey的request正在請求,那麼就把目前request堆積起來,堆積起來之後呢?當然是等待上一個request完成啊。NetworkDispatcher中有如下代碼

private void processRequest() throws InterruptedException {
        // Take a request from the queue.
        Request<?> request = mQueue.take();
        long startTimeMs = SystemClock.elapsedRealtime();
        try {
            request.addMarker("network-queue-take");

            // If the request was cancelled already, do not perform the
            // network request.
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                request.notifyListenerResponseNotUsable();
                return;
            }

            addTrafficStatsTag(request);

            // Perform the network request.
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            request.addMarker("network-http-complete");
            // If the server returned 304 AND we delivered a response already,
            // we're done -- don't deliver a second identical response.
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                request.notifyListenerResponseNotUsable();
                return;
            }
            // Parse the response here on the worker thread.
            Response<?> response = request.parseNetworkResponse(networkResponse);
            request.addMarker("network-parse-complete");
            // Write to cache if applicable.
            // TODO: Only update cache metadata instead of entire record for 304s.
//            if (request.shouldCache() && response.cacheEntry != null) {
//                mCache.put(request.getCacheKey(), response.cacheEntry);
//                request.addMarker("network-cache-written");
//            }
            // Post the response back.
            request.markDelivered();
            mDelivery.postResponse(request, response);
            request.notifyListenerResponseReceived(response);
        } catch (VolleyError volleyError) {
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            parseAndDeliverNetworkError(request, volleyError);
            request.notifyListenerResponseNotUsable();
        } catch (Exception e) {
            VolleyLog.e(e, "Unhandled exception %s", e.toString());
            VolleyError volleyError = new VolleyError(e);
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            mDelivery.postError(request, volleyError);
            request.notifyListenerResponseNotUsable();
        }
    }
           

注意這一句代碼:request.notifyListenerResponseNotUsable();可以看到在很多地方都調用了這一句,那麼這個方法的具體實作是怎樣的呢?

/**
     * Notify NetworkRequestCompleteListener that the network request did not result in a response
     * which can be used for other, waiting requests.
     */
    /* package */ void notifyListenerResponseNotUsable() {
        NetworkRequestCompleteListener listener;
        synchronized (mLock) {
            listener = mRequestCompleteListener;
        }
        if (listener != null) {
            listener.onNoUsableResponseReceived(this);
        }
    }
           

這裡回調了listener.onNoUsableResponseReceived(this);這個方法的實作在那裡呢?想必很容易能夠猜到了,就在CacheDispatcher中,最後來看看CacheDispatcher中比較重要的兩個方法:

/**
         * No valid response received from network, release waiting requests.
         */
        @Override
        public synchronized void onNoUsableResponseReceived(Request<?> request) {
            String cacheKey = request.getCacheKey();
            List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
            if (waitingRequests != null && !waitingRequests.isEmpty()) {
                if (VolleyLog.DEBUG) {
                    VolleyLog.v(
                            "%d waiting requests for cacheKey=%s; resend to network",
                            waitingRequests.size(), cacheKey);
                }
                Request<?> nextInLine = waitingRequests.remove();
                mWaitingRequests.put(cacheKey, waitingRequests);
                nextInLine.setNetworkRequestCompleteListener(this);
                try {
                    mCacheDispatcher.mNetworkQueue.put(nextInLine);
                } catch (InterruptedException iex) {
                    VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
                    // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
                    Thread.currentThread().interrupt();
                    // Quit the current CacheDispatcher thread.
                    mCacheDispatcher.quit();
                }
            }
        }

        /**
         * For cacheable requests, if a request for the same cache key is already in flight, add it
         * to a queue to wait for that in-flight request to finish.
         *
         * @return whether the request was queued. If false, we should continue issuing the request
         * over the network. If true, we should put the request on hold to be processed when the
         * in-flight request finishes.
         */
        private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
            String cacheKey = request.getCacheKey();
            // Insert request into stage if there's already a request with the same cache key
            // in flight.
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new ArrayList<Request<?>>();
                }
                request.addMarker("waiting-for-response");
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
                return true;
            } else {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                request.setNetworkRequestCompleteListener(this);
                if (VolleyLog.DEBUG) {
                    VolleyLog.d("new request, sending to network %s", cacheKey);
                }
                return false;
            }
        }
           

這裡可以看到在最開始取到Entry的時候,就調用過maybeAddToWaitingRequests()方法進行判斷,并且在方法中儲存了CacheKey相同的request,然後當第一個request請求完成的時候回調onNoUsableResponseReceived()方法,如果存在有效緩存則取出緩存進行結果回調,如果無有效緩存,則再繼續進行網絡請求。

最後也就解釋了,為何使用Volley對同一個接口同時請求多次的時候網絡線程并非并行,而是串行的問題了。

總結

其實每一個開源庫裡面的實作都是非常具有參考價值的,有時候看起來很簡單,但是裡面很多技術細節是需要我們注意和學習的,比如說上面代碼中有一個叫做PriorityBlockingQueue的類,可能大部分人也不知道這個類的特點是什麼,或許根本就沒注意過這一個類。而我說來也很慚愧,如果不是遇見這個bug,可能我也不會刨根究底的來檢視Volley的源碼,以前看了個大概,覺得已經很了解這個庫了,覺得這個庫的實作是很簡單的,然而很多細節的地方是真的沒有注意到的。這其實也就暴露出我們絕大部分開發者的一個通病,拿着一個庫隻會去檢視怎麼使用,而很少去檢視它的功能在代碼中是如何實作的。